Use rhosts file to grant permissions to users.

Whenever a user logs in through CAS, their username is looked up in the
.rhosts file provided through settings. If it is found, the user is
granted staff status and BOcal group (cf fixture).

The rights are re-evaluated at each login and at each access to
/admin/login.

Close #2.
This commit is contained in:
Théophile Bastian 2017-10-14 19:36:13 +02:00
parent e699fc617e
commit eed3653d22
13 changed files with 173 additions and 2 deletions

1
.gitignore vendored
View file

@ -106,3 +106,4 @@ ENV/
# mypy # mypy
.mypy_cache/ .mypy_cache/
rhosts_dev

View file

@ -45,6 +45,7 @@ nécessaires en base de données :
```bash ```bash
cd .. # Pour revenir dans bocal-site cd .. # Pour revenir dans bocal-site
./manage.py migrate ./manage.py migrate
./manage.py loaddata bocal_group
``` ```
À partir de là, ça tourne :) Il ne reste plus qu'à faire tourner soit avec un À partir de là, ça tourne :) Il ne reste plus qu'à faire tourner soit avec un

View file

@ -43,3 +43,6 @@ CAS_FORCE_CHANGE_USERNAME_CASE = 'lower'
CAS_REDIRECT_URL = '/' CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
CAS_LOGOUT_COMPLETELY = False CAS_LOGOUT_COMPLETELY = False
# Auth
RHOSTS_PATH = 'rhosts_dev'

View file

@ -54,3 +54,6 @@ CAS_FORCE_CHANGE_USERNAME_CASE = 'lower'
CAS_REDIRECT_URL = '/' CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" # FIXME CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" # FIXME
CAS_LOGOUT_COMPLETELY = False CAS_LOGOUT_COMPLETELY = False
# Auth
RHOSTS_PATH = '' # FIXME (path to BOcal's .rhosts)

View file

@ -20,6 +20,7 @@ import django.contrib.auth.views as dj_auth_views
import mainsite.urls import mainsite.urls
import bocal_auth.views as auth_views import bocal_auth.views as auth_views
from bocal_auth.rhosts import forceReevalRhosts
import markdownx.urls import markdownx.urls
import api.urls import api.urls
@ -27,7 +28,7 @@ import django_cas_ng.views
# Force the user to login through the custom login page # Force the user to login through the custom login page
admin.site.login = login_required(admin.site.login) admin.site.login = login_required(forceReevalRhosts(admin.site.login))
cas_patterns = [ cas_patterns = [
url(r'^login$', django_cas_ng.views.login, name='cas_ng_login'), url(r'^login$', django_cas_ng.views.login, name='cas_ng_login'),

View file

@ -0,0 +1 @@
default_app_config = 'bocal_auth.apps.BocalAuthConfig'

View file

@ -1,5 +1,8 @@
from django.apps import AppConfig from django.apps import AppConfig
class AuthConfig(AppConfig): class BocalAuthConfig(AppConfig):
name = 'bocal_auth' name = 'bocal_auth'
def ready(self):
from . import signals

View file

@ -1,7 +1,13 @@
from django_cas_ng.backends import CASBackend from django_cas_ng.backends import CASBackend
from .models import CasUser
from . import rhosts
class BOcalCASBackend(CASBackend): class BOcalCASBackend(CASBackend):
# Partly from Robin Champenois's "ExperiENS". Thanks! # Partly from Robin Champenois's "ExperiENS". Thanks!
def clean_username(self, username): def clean_username(self, username):
return username.lower().strip() return username.lower().strip()
def configure_user(self, user):
casUser = CasUser(user=user)
casUser.save()

View file

@ -0,0 +1 @@
[{"model": "auth.group", "pk": 1, "fields": {"name": "BOcal", "permissions": [28, 29, 30, 19, 20, 21, 22, 23, 24, 25, 26, 27]}}]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-10-14 16:49
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='CasUser',
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
],
),
]

11
bocal_auth/models.py Normal file
View file

@ -0,0 +1,11 @@
from django.db import models
from django.contrib.auth.models import User
class CasUser(models.Model):
''' Describes a Django user that was created through CAS '''
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
primary_key=True)

99
bocal_auth/rhosts.py Normal file
View file

@ -0,0 +1,99 @@
''' Reads a .rhosts file '''
from django.conf import settings
from django.contrib.auth.models import Group
from .models import CasUser
def hasUser(user, allowed_domains=[]):
''' Check that `user` appears in the rhosts file.
If `allowed_domains` is not empty, also checks that the user belongs to one
of the specified domains. '''
def clearLine(line):
line = line.strip()
hashPos = line.find('#')
if hashPos >= 0:
line = line[:hashPos]
return line
with open(settings.RHOSTS_PATH, 'r') as handle:
for line in handle:
line = clearLine(line)
if not line:
continue
spl = line.split()
if len(spl) != 2:
continue # Not a login line
domain, login = spl
if login != user: # Not the ones we're looking for
continue
if domain[:2] != '+@': # Not a valid domain
continue
domain = domain[2:]
if allowed_domains != [] and domain not in allowed_domains:
continue
return True
return False
def default_allowed(user):
return hasUser(user, allowed_domains=['eleves'])
class NoBOcalException(Exception):
def __str__():
return "The BOcal group was not created"
def bocalGroup():
qs = Group.objects.filter(name='BOcal')
if qs.count() != 1:
raise NoBOcalException
return qs[0]
def stripCasPrivileges(user):
user.groups.remove(bocalGroup())
user.is_staff = False
user.save()
def grantBOcalPrivileges(user):
user.is_staff = True
user.groups.add(bocalGroup())
user.save()
def requireCasUser(fct):
def wrap(user, *args, **kwargs):
qs = CasUser.objects.filter(user=user)
if not qs.count() > 0:
return
return fct(user, *args, **kwargs)
return wrap
@requireCasUser
def evalRhostsPrivileges(user):
if default_allowed(user.username):
grantBOcalPrivileges(user)
else:
stripCasPrivileges(user)
@requireCasUser
def logout(user):
stripCasPrivileges()
def forceReevalRhosts(fct):
def wrap(req, *args, **kwargs):
evalRhostsPrivileges(req.user)
return fct(req, *args, **kwargs)
return wrap

16
bocal_auth/signals.py Normal file
View file

@ -0,0 +1,16 @@
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated, cas_user_logout
from . import rhosts
@receiver(cas_user_authenticated)
def onCasLogin(sender, user, **kwargs):
''' Called upon login of a user through CAS '''
rhosts.evalRhostsPrivileges(user)
@receiver(cas_user_logout)
def onCasLogout(sender, user, **kwargs):
''' Strip the user from their privileges — in case something goes wrong
during the next authentication '''
rhosts.logout(user)