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
|
||||
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 = [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue