Meilleurs noms de variables, meilleure doc

This commit is contained in:
Martin Pépin 2020-05-17 14:02:33 +02:00
parent 470bca4d1f
commit e9e8fe8d56
4 changed files with 61 additions and 52 deletions

View file

@ -16,7 +16,6 @@ Plus précisément :
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é.
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
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.backends.ENSCASBackend"` dans les
[`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
AUTHENTICATION_BACKENDS = [

View file

@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model
from django.db import transaction
from authens.models import Clipper
from authens.models import CASAccount
from authens.utils import get_cas_client
UserModel = get_user_model()
@ -12,7 +12,11 @@ class ENSCASError(Exception):
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")
if home_dir is None:
@ -22,8 +26,9 @@ def get_entrance_year(attributes):
if len(dirs) < 3 or not dirs[2].isdecimal():
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.
year = int(dirs[2])
if year >= 80:
return 1900 + year
else:
@ -31,19 +36,18 @@ def get_entrance_year(attributes):
class ENSCASBackend:
"""ENSAuth authentication backend.
"""AuthENS CAS authentication backend.
Implement standard CAS v3 authentication and handles username clashes with non-CAS
accounts and potential old CAS accounts.
Every user connecting via CAS is given a `authens.models.Clipper` instance which
remembers her clipper login and her entrance year (the year her clipper account was
Every user connecting via CAS is given an `authens.models.CASAccount` instance which
remembers her CAS login and her entrance year (the year her CAS account was
created).
At each connection, we search for a Clipper account with the given clipper login
(uid) and create one if none exists. In case the Clipper account's entrance year
does not match the entrance year given by CAS, it means it is a old account and it
must be deleted. The corresponding user can still connect using regular Django
authentication.
At each connection, we search for a CAS account with the given CAS login and create
one if none exists. In case the CAS account's entrance year does not match the
entrance year given by CAS, it means it is a old account and it must be deleted. The
corresponding user can still connect using regular Django authentication.
"""
def authenticate(self, request, ticket=None):
@ -57,8 +61,8 @@ class ENSCASBackend:
year = get_entrance_year(attributes)
return self._get_or_create(uid, year)
def get_free_username(self, clipper_uid):
"""Find an available username for the new user.
def get_free_username(self, cas_login):
"""Find an available username for a new user.
If you override this method, make sure it returns a username that is not taken
by any existing user.
@ -69,35 +73,46 @@ class ENSCASBackend:
taken = UserModel.objects.values_list("username", flat=True)
# 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)
for name in prefered:
if name not in pref_taken:
return name
# Worst case: generate a username of the form clipper_uid + int
taken = taken.filter(username__startswith=clipper_uid)
# Worst case: generate a username of the form cas_login + int
taken = taken.filter(username__startswith=cas_login)
i = 2
while clipper_uid + str(i) in taken:
while cas_login + str(i) in taken:
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():
try:
user = UserModel.objects.get(clipper__uid=uid)
if user.clipper.entrance_year != entrance_year:
user.clipper.delete()
user = UserModel.objects.get(cas_account__cas_login=cas_login)
if user.cas_account.entrance_year != entrance_year:
user.cas_account.delete()
user = None
except UserModel.DoesNotExist:
user = 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)
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
# Django boilerplate.
def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id)

View file

@ -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.db import migrations, models
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name="Clipper",
name="CASAccount",
fields=[
(
"id",
@ -27,30 +27,30 @@ class Migration(migrations.Migration):
),
),
(
"uid",
"cas_login",
models.CharField(
max_length=1023, unique=True, verbose_name="login clipper"
max_length=1023, unique=True, verbose_name="login CAS"
),
),
(
"entrance_year",
models.SmallIntegerField(
verbose_name="année de création du compte clipper"
verbose_name="année de création du compte CAS"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="clipper",
related_name="cas_account",
to=settings.AUTH_USER_MODEL,
verbose_name="utilisateurice",
),
),
],
options={
"verbose_name": "Compte clipper",
"verbose_name_plural": "Comptes clipper",
"verbose_name": "Compte CAS",
"verbose_name_plural": "Comptes CAS",
},
),
]

View file

@ -5,40 +5,35 @@ from django.utils.translation import gettext_lazy as _
User = get_user_model()
class Clipper(models.Model):
"""Information about clipper accounts.
class CASAccount(models.Model):
"""Information about CAS accounts.
A user is given an instance of this model iff it has a clipper account. The `uid`
field is the clipper login and is used for CAS authentication.
A user is given an instance of this model iff she has a CAS account.
At each connection, we check that the `entrance_year` we have is consistent with the
meta-data returned by the SPI's CAS.
- 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.
Instances of this model should only be created by the `ENSCASBackend` authentication
backend.
"""
user = models.OneToOneField(
User,
verbose_name=_("utilisateurice"),
on_delete=models.CASCADE,
related_name="clipper",
related_name="cas_account",
)
uid = models.CharField(
verbose_name=_("login clipper"), max_length=1023, blank=False, unique=True,
cas_login = models.CharField(
verbose_name=_("login CAS"), max_length=1023, blank=False, unique=True,
)
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:
verbose_name = _("Compte clipper")
verbose_name_plural = _("Comptes clipper")
verbose_name = _("Compte CAS")
verbose_name_plural = _("Comptes CAS")
def __str__(self):
return _("compte clipper %(uid)s@%(entrance_year)s lié à %(user)s") % {
"uid": self.uid,
return _("compte CAS %(cas_login) (promo %(entrance_year)s) lié à %(user)s") % {
"cas_login": self.cas_login,
"entrance_year": self.entrance_year,
"user": self.user.username,
}