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:
parent
e699fc617e
commit
eed3653d22
13 changed files with 173 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -106,3 +106,4 @@ ENV/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
rhosts_dev
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'bocal_auth.apps.BocalAuthConfig'
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
1
bocal_auth/fixtures/bocal_group.json
Normal file
1
bocal_auth/fixtures/bocal_group.json
Normal 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]}}]
|
25
bocal_auth/migrations/0001_initial.py
Normal file
25
bocal_auth/migrations/0001_initial.py
Normal 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
11
bocal_auth/models.py
Normal 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
99
bocal_auth/rhosts.py
Normal 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
16
bocal_auth/signals.py
Normal 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)
|
Loading…
Reference in a new issue