Meilleurs noms de variables, meilleure doc
This commit is contained in:
parent
470bca4d1f
commit
e9e8fe8d56
4 changed files with 61 additions and 52 deletions
|
@ -16,7 +16,6 @@ Plus précisément :
|
||||||
choisira d'utiliser la connexion par mot de passe sur le site, typiquement
|
choisira d'utiliser la connexion par mot de passe sur le site, typiquement
|
||||||
après la fin de la scolarité lorsque le compte clipper est supprimé.
|
après la fin de la scolarité lorsque le compte clipper est supprimé.
|
||||||
|
|
||||||
|
|
||||||
2. Si, quelques années plus tard, après que `dupond` a terminé sa scolarité, le
|
2. Si, quelques années plus tard, après que `dupond` a terminé sa scolarité, le
|
||||||
SPI donne le login `dupond` à une nouvelle personne, AuthENS détecte que le
|
SPI donne le login `dupond` à une nouvelle personne, AuthENS détecte que le
|
||||||
nouveau compte `dupond` n'est pas le même que l'ancien et crée un nouveau
|
nouveau compte `dupond` n'est pas le même que l'ancien et crée un nouveau
|
||||||
|
@ -50,7 +49,7 @@ Django sous le nom `"authens:logout"`.
|
||||||
- Ajouter `"authens"` dans les [`INSTALLED_APPS`](https://docs.djangoproject.com/en/3.0/ref/settings/#installed-apps).
|
- Ajouter `"authens"` dans les [`INSTALLED_APPS`](https://docs.djangoproject.com/en/3.0/ref/settings/#installed-apps).
|
||||||
- Ajouter `"authens.backends.ENSCASBackend"` dans les
|
- Ajouter `"authens.backends.ENSCASBackend"` dans les
|
||||||
[`AUTHENTICATION_BACKENDS`](https://docs.djangoproject.com/en/3.0/ref/settings/#authentication-backends).
|
[`AUTHENTICATION_BACKENDS`](https://docs.djangoproject.com/en/3.0/ref/settings/#authentication-backends).
|
||||||
Si `AUTHENTICATION_BACKENDS` n'apparaît pas dans vos settings, utilisez :
|
Si `AUTHENTICATION_BACKENDS` n'apparaît pas dans vos settings, utiliser :
|
||||||
|
|
||||||
```python
|
```python
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from authens.models import Clipper
|
from authens.models import CASAccount
|
||||||
from authens.utils import get_cas_client
|
from authens.utils import get_cas_client
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
@ -12,7 +12,11 @@ class ENSCASError(Exception):
|
||||||
|
|
||||||
|
|
||||||
def get_entrance_year(attributes):
|
def get_entrance_year(attributes):
|
||||||
"""Infer the entrance year of a clipper account holder from its home directory."""
|
"""Infer the entrance year of a CAS account holder from her home directory."""
|
||||||
|
|
||||||
|
# The home directory of a user is of the form /users/YEAR/DEPARTMENT/CAS_LOGIN where
|
||||||
|
# YEAR is a 2-digit number representing the entrance year of the student. We get the
|
||||||
|
# entrance year from there.
|
||||||
|
|
||||||
home_dir = attributes.get("homeDirectory")
|
home_dir = attributes.get("homeDirectory")
|
||||||
if home_dir is None:
|
if home_dir is None:
|
||||||
|
@ -22,8 +26,9 @@ def get_entrance_year(attributes):
|
||||||
if len(dirs) < 3 or not dirs[2].isdecimal():
|
if len(dirs) < 3 or not dirs[2].isdecimal():
|
||||||
raise ENSCASError("Invalid homeDirectory: {}".format(home_dir))
|
raise ENSCASError("Invalid homeDirectory: {}".format(home_dir))
|
||||||
|
|
||||||
year = int(dirs[2])
|
# Expand the 2-digit entrance year into 4 digits.
|
||||||
# This will break in 2080.
|
# This will break in 2080.
|
||||||
|
year = int(dirs[2])
|
||||||
if year >= 80:
|
if year >= 80:
|
||||||
return 1900 + year
|
return 1900 + year
|
||||||
else:
|
else:
|
||||||
|
@ -31,19 +36,18 @@ def get_entrance_year(attributes):
|
||||||
|
|
||||||
|
|
||||||
class ENSCASBackend:
|
class ENSCASBackend:
|
||||||
"""ENSAuth authentication backend.
|
"""AuthENS CAS authentication backend.
|
||||||
|
|
||||||
Implement standard CAS v3 authentication and handles username clashes with non-CAS
|
Implement standard CAS v3 authentication and handles username clashes with non-CAS
|
||||||
accounts and potential old CAS accounts.
|
accounts and potential old CAS accounts.
|
||||||
|
|
||||||
Every user connecting via CAS is given a `authens.models.Clipper` instance which
|
Every user connecting via CAS is given an `authens.models.CASAccount` instance which
|
||||||
remembers her clipper login and her entrance year (the year her clipper account was
|
remembers her CAS login and her entrance year (the year her CAS account was
|
||||||
created).
|
created).
|
||||||
At each connection, we search for a Clipper account with the given clipper login
|
At each connection, we search for a CAS account with the given CAS login and create
|
||||||
(uid) and create one if none exists. In case the Clipper account's entrance year
|
one if none exists. In case the CAS account's entrance year does not match the
|
||||||
does not match the entrance year given by CAS, it means it is a old account and it
|
entrance year given by CAS, it means it is a old account and it must be deleted. The
|
||||||
must be deleted. The corresponding user can still connect using regular Django
|
corresponding user can still connect using regular Django authentication.
|
||||||
authentication.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def authenticate(self, request, ticket=None):
|
def authenticate(self, request, ticket=None):
|
||||||
|
@ -57,8 +61,8 @@ class ENSCASBackend:
|
||||||
year = get_entrance_year(attributes)
|
year = get_entrance_year(attributes)
|
||||||
return self._get_or_create(uid, year)
|
return self._get_or_create(uid, year)
|
||||||
|
|
||||||
def get_free_username(self, clipper_uid):
|
def get_free_username(self, cas_login):
|
||||||
"""Find an available username for the new user.
|
"""Find an available username for a new user.
|
||||||
|
|
||||||
If you override this method, make sure it returns a username that is not taken
|
If you override this method, make sure it returns a username that is not taken
|
||||||
by any existing user.
|
by any existing user.
|
||||||
|
@ -69,35 +73,46 @@ class ENSCASBackend:
|
||||||
taken = UserModel.objects.values_list("username", flat=True)
|
taken = UserModel.objects.values_list("username", flat=True)
|
||||||
|
|
||||||
# This should handle most cases and produce a nice username.
|
# This should handle most cases and produce a nice username.
|
||||||
prefered = [clipper_uid, "cas_" + clipper_uid]
|
prefered = [cas_login, "cas_" + cas_login]
|
||||||
pref_taken = taken.filter(username__in=prefered)
|
pref_taken = taken.filter(username__in=prefered)
|
||||||
for name in prefered:
|
for name in prefered:
|
||||||
if name not in pref_taken:
|
if name not in pref_taken:
|
||||||
return name
|
return name
|
||||||
|
|
||||||
# Worst case: generate a username of the form clipper_uid + int
|
# Worst case: generate a username of the form cas_login + int
|
||||||
taken = taken.filter(username__startswith=clipper_uid)
|
taken = taken.filter(username__startswith=cas_login)
|
||||||
i = 2
|
i = 2
|
||||||
while clipper_uid + str(i) in taken:
|
while cas_login + str(i) in taken:
|
||||||
i += 1
|
i += 1
|
||||||
return clipper_uid + str(i)
|
return cas_login + str(i)
|
||||||
|
|
||||||
|
def _get_or_create(self, cas_login, entrance_year):
|
||||||
|
"""Handles account retrieval, creation and invalidation as described above.
|
||||||
|
|
||||||
|
- If no CAS account exists, create one;
|
||||||
|
- If a CAS account exists, but with the wrong entrance year, remove it and
|
||||||
|
create a new one;
|
||||||
|
- If a matching CAS account exists, retrieve it.
|
||||||
|
"""
|
||||||
|
|
||||||
def _get_or_create(self, uid, entrance_year):
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
user = UserModel.objects.get(clipper__uid=uid)
|
user = UserModel.objects.get(cas_account__cas_login=cas_login)
|
||||||
if user.clipper.entrance_year != entrance_year:
|
if user.cas_account.entrance_year != entrance_year:
|
||||||
user.clipper.delete()
|
user.cas_account.delete()
|
||||||
user = None
|
user = None
|
||||||
except UserModel.DoesNotExist:
|
except UserModel.DoesNotExist:
|
||||||
user = None
|
user = None
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
username = self.get_free_username(uid)
|
username = self.get_free_username(cas_login)
|
||||||
user = UserModel.objects.create_user(username=username)
|
user = UserModel.objects.create_user(username=username)
|
||||||
Clipper.objects.create(user=user, entrance_year=entrance_year, uid=uid)
|
CASAccount.objects.create(
|
||||||
|
user=user, entrance_year=entrance_year, cas_login=cas_login
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
# Django boilerplate.
|
||||||
def get_user(self, user_id):
|
def get_user(self, user_id):
|
||||||
try:
|
try:
|
||||||
return UserModel.objects.get(pk=user_id)
|
return UserModel.objects.get(pk=user_id)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.6 on 2020-05-10 19:14
|
# Generated by Django 3.0.6 on 2020-05-17 11:58
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Clipper",
|
name="CASAccount",
|
||||||
fields=[
|
fields=[
|
||||||
(
|
(
|
||||||
"id",
|
"id",
|
||||||
|
@ -27,30 +27,30 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"uid",
|
"cas_login",
|
||||||
models.CharField(
|
models.CharField(
|
||||||
max_length=1023, unique=True, verbose_name="login clipper"
|
max_length=1023, unique=True, verbose_name="login CAS"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"entrance_year",
|
"entrance_year",
|
||||||
models.SmallIntegerField(
|
models.SmallIntegerField(
|
||||||
verbose_name="année de création du compte clipper"
|
verbose_name="année de création du compte CAS"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
models.OneToOneField(
|
models.OneToOneField(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="clipper",
|
related_name="cas_account",
|
||||||
to=settings.AUTH_USER_MODEL,
|
to=settings.AUTH_USER_MODEL,
|
||||||
verbose_name="utilisateurice",
|
verbose_name="utilisateurice",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Compte clipper",
|
"verbose_name": "Compte CAS",
|
||||||
"verbose_name_plural": "Comptes clipper",
|
"verbose_name_plural": "Comptes CAS",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,40 +5,35 @@ from django.utils.translation import gettext_lazy as _
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class Clipper(models.Model):
|
class CASAccount(models.Model):
|
||||||
"""Information about clipper accounts.
|
"""Information about CAS accounts.
|
||||||
|
|
||||||
A user is given an instance of this model iff it has a clipper account. The `uid`
|
A user is given an instance of this model iff she has a CAS account.
|
||||||
field is the clipper login and is used for CAS authentication.
|
|
||||||
|
|
||||||
At each connection, we check that the `entrance_year` we have is consistent with the
|
Instances of this model should only be created by the `ENSCASBackend` authentication
|
||||||
meta-data returned by the SPI's CAS.
|
backend.
|
||||||
- if both entrance years match, we connect `self.user`
|
|
||||||
- if not, we consider that this account is old and assume a new clipper account with
|
|
||||||
the same id has been created. We remove this instance and create a new user
|
|
||||||
associated with a new Clipper account.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User,
|
User,
|
||||||
verbose_name=_("utilisateurice"),
|
verbose_name=_("utilisateurice"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="clipper",
|
related_name="cas_account",
|
||||||
)
|
)
|
||||||
uid = models.CharField(
|
cas_login = models.CharField(
|
||||||
verbose_name=_("login clipper"), max_length=1023, blank=False, unique=True,
|
verbose_name=_("login CAS"), max_length=1023, blank=False, unique=True,
|
||||||
)
|
)
|
||||||
entrance_year = models.SmallIntegerField(
|
entrance_year = models.SmallIntegerField(
|
||||||
verbose_name=_("année de création du compte clipper"), blank=False, null=False
|
verbose_name=_("année de création du compte CAS"), blank=False, null=False
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Compte clipper")
|
verbose_name = _("Compte CAS")
|
||||||
verbose_name_plural = _("Comptes clipper")
|
verbose_name_plural = _("Comptes CAS")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("compte clipper %(uid)s@%(entrance_year)s lié à %(user)s") % {
|
return _("compte CAS %(cas_login) (promo %(entrance_year)s) lié à %(user)s") % {
|
||||||
"uid": self.uid,
|
"cas_login": self.cas_login,
|
||||||
"entrance_year": self.entrance_year,
|
"entrance_year": self.entrance_year,
|
||||||
"user": self.user.username,
|
"user": self.user.username,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue