Intégration AuthENS
This commit is contained in:
parent
79eb294ce5
commit
5644ea9290
12 changed files with 116 additions and 146 deletions
|
@ -1,57 +0,0 @@
|
|||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
from allauth_ens.adapter import LongTermClipperAccountAdapter, get_ldap_infos
|
||||
|
||||
class AccountAdapter(DefaultAccountAdapter):
|
||||
def is_open_for_signup(self, request):
|
||||
return False
|
||||
|
||||
|
||||
class SocialAccountAdapter(LongTermClipperAccountAdapter):
|
||||
def is_open_for_signup(self, request, sociallogin):
|
||||
# sociallogin.account is a SocialAccount instance.
|
||||
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py
|
||||
|
||||
if sociallogin.account.provider == 'clipper':
|
||||
return True
|
||||
|
||||
# It returns AccountAdapter.is_open_for_signup().
|
||||
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py
|
||||
return super().is_open_for_signup(request, sociallogin)
|
||||
|
||||
|
||||
# TODO : HOTFIX pour un bug d'allauth_ens
|
||||
# On remplace la déduplication des comptes faites avec "entrance_year"
|
||||
# par une déduplication sur le nom d'utilisateur
|
||||
# (Copié de allauth_ens)
|
||||
def pre_social_login(self, request, sociallogin):
|
||||
if sociallogin.account.provider != "clipper":
|
||||
return super(LongTermClipperAccountAdapter,
|
||||
self).pre_social_login(request, sociallogin)
|
||||
|
||||
clipper_uid = sociallogin.account.uid
|
||||
try:
|
||||
old_conn = SocialAccount.objects.get(provider='clipper_inactive',
|
||||
uid=clipper_uid)
|
||||
except SocialAccount.DoesNotExist:
|
||||
return
|
||||
|
||||
ldap_data = get_ldap_infos(clipper_uid)
|
||||
sociallogin._ldap_data = ldap_data
|
||||
|
||||
if ldap_data is None or 'entrance_year' not in ldap_data:
|
||||
raise ValueError("No entrance year in LDAP data")
|
||||
|
||||
old_conn_username = old_conn.user.username
|
||||
|
||||
# HOTFIX ICI
|
||||
if self.get_username(clipper_uid, ldap_data) != old_conn_username:
|
||||
email = ldap_data.get('email', get_clipper_email(clipper_uid))
|
||||
remove_email(old_conn.user, email)
|
||||
|
||||
return
|
||||
|
||||
old_conn.provider = 'clipper'
|
||||
old_conn.save()
|
||||
|
||||
sociallogin.lookup()
|
|
@ -1,7 +1,4 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AvisstageConfig(AppConfig):
|
||||
name = 'avisstage'
|
||||
|
|
12
avisstage/management/commands/termine_scolarite.py
Normal file
12
avisstage/management/commands/termine_scolarite.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
from avisstage.models import Normalien
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Réinitialise les statuts "en scolarité" de tout le monde'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
return
|
||||
|
||||
def handle(self, *args, **options):
|
||||
Normalien.objects.all().update(en_scolarite=False)
|
||||
self.stdout.write(self.style.SUCCESS(u'Terminé'))
|
18
avisstage/migrations/0005_normalien_en_scolarite.py
Normal file
18
avisstage/migrations/0005_normalien_en_scolarite.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.17 on 2021-01-17 20:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('avisstage', '0004_allauth_to_authens'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='normalien',
|
||||
name='en_scolarite',
|
||||
field=models.BooleanField(blank=True, default=False),
|
||||
),
|
||||
]
|
|
@ -1,10 +1,3 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -20,10 +13,14 @@ from django.utils.html import strip_tags
|
|||
from taggit_autosuggest.managers import TaggableManager
|
||||
from tinymce.models import HTMLField as RichTextField
|
||||
|
||||
from .utils import choices_length
|
||||
from .statics import DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT, TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT
|
||||
from authens.signals import post_cas_connect
|
||||
from authens.models import CASAccount
|
||||
|
||||
import ldap
|
||||
from .utils import choices_length, is_email_ens
|
||||
from .statics import (
|
||||
DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT,
|
||||
TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT
|
||||
)
|
||||
|
||||
#
|
||||
# Profil Normalien (extension du modèle User)
|
||||
|
@ -40,7 +37,8 @@ class Normalien(models.Model):
|
|||
max_length=200, blank=True)
|
||||
contactez_moi = models.BooleanField(u"Inviter les visiteurs à me contacter",
|
||||
default=True)
|
||||
bio = models.TextField(u"À propos de moi", blank=True, default="");
|
||||
bio = models.TextField(u"À propos de moi", blank=True, default="")
|
||||
en_scolarite = models.BooleanField(default=False, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = u"Profil élève"
|
||||
|
@ -53,53 +51,62 @@ class Normalien(models.Model):
|
|||
def stages_publics(self):
|
||||
return self.stages.filter(public=True).order_by('-date_debut')
|
||||
|
||||
@cached_property
|
||||
def en_scolarite(self):
|
||||
return SocialAccount.objects.filter(user_id=self.user_id,
|
||||
provider="clipper").exists()
|
||||
|
||||
def has_nonENS_email(self):
|
||||
a = EmailAddress.objects.filter(user_id=self.user_id,
|
||||
verified=True) \
|
||||
.exclude(email__endswith="ens.fr")
|
||||
return a.exists()
|
||||
return not (
|
||||
is_email_ens(self.mail, True)
|
||||
and is_email_ens(self.user.email, True)
|
||||
)
|
||||
|
||||
def nom_complet(self):
|
||||
if self.nom.strip():
|
||||
return self.nom
|
||||
return self.user.username
|
||||
|
||||
@property
|
||||
def preferred_email(self):
|
||||
a = EmailAddress.objects.filter(user_id=self.user_id,
|
||||
verified=True) \
|
||||
.exclude(email__endswith="ens.fr")\
|
||||
.order_by('-primary')
|
||||
if len(a) == 0:
|
||||
a = EmailAddress.objects.filter(user_id=self.user_id,
|
||||
verified=True) \
|
||||
.order_by('-primary')
|
||||
if len(a) == 0:
|
||||
return ""
|
||||
else:
|
||||
return a[0].email
|
||||
return self.user.email
|
||||
|
||||
# Hook à la création d'un nouvel utilisateur : récupération de ses infos par LDAP
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
# Hook à la création d'un nouvel utilisateur : information de base
|
||||
def create_basic_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
profil, created = Normalien.objects.get_or_create(user=instance)
|
||||
try:
|
||||
saccount = SocialAccount.objects.get(user=instance,
|
||||
provider="clipper")
|
||||
except SocialAccount.DoesNotExist:
|
||||
|
||||
if not created and profil.promotion != "":
|
||||
return
|
||||
|
||||
if "@" in instance.username:
|
||||
profil.promotion = instance.username.split("@")[1]
|
||||
profil.save()
|
||||
|
||||
post_save.connect(create_basic_user_profile, sender=User)
|
||||
|
||||
# Hook d'authENS : information du CAS
|
||||
def handle_cas_connection(sender, instance, created, cas_login, attributes, **kwargs):
|
||||
profil, created = Normalien.objects.get_or_create(user=instance)
|
||||
|
||||
if not created:
|
||||
if not profil.en_scolarite:
|
||||
profil.en_scolarite = True
|
||||
profil.save()
|
||||
return
|
||||
edata = saccount.extra_data.get("ldap", {})
|
||||
dep = ""
|
||||
if "department_code" in edata:
|
||||
dep = dict(DEPARTEMENTS_DEFAUT).get(
|
||||
edata["department_code"].lower(), '')
|
||||
|
||||
profil.promotion = "%s %s" % (dep, edata["entrance_year"])
|
||||
profil.nom = edata.get("name", "")
|
||||
dirs = attributes.get("homeDirectory", "").split("/")
|
||||
if len(dirs) < 4:
|
||||
print("HomeDirectory invalide", dirs)
|
||||
return
|
||||
|
||||
year = dirs[2]
|
||||
departement = dirs[3]
|
||||
print(departement, dirs)
|
||||
|
||||
dep = dict(DEPARTEMENTS_DEFAUT).get(departement.lower(), "")
|
||||
|
||||
profil.promotion = "%s %s" % (dep, year)
|
||||
profil.nom = attributes.get("name", "")
|
||||
profil.en_scolarite = True
|
||||
profil.save()
|
||||
|
||||
post_save.connect(create_user_profile, sender=User)
|
||||
post_cas_connect.connect(handle_cas_connection, sender=User)
|
||||
|
||||
#
|
||||
# Lieu de stage
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
<script type="text/javascript" src="{% static "js/render.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Profil de {{ object.nom }} - ExperiENS{% endblock %}
|
||||
{% block title %}Profil de {{ object.nom_complet }} - ExperiENS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Profil de {{ object.nom }}
|
||||
<h1>Profil de {{ object.nom_complet }}
|
||||
{% if object.user == user %}
|
||||
<a href="{% url "avisstage:profil_edit" %}" class="btn edit-btn">Modifier mes infos</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<h1>{{ object.sujet }}</h1>
|
||||
<p class="dates"><span class="year">{{ object.date_debut|date:"Y" }}</span><span class="debut">{{ object.date_debut|date:"d/m" }}</span><span class="fin">{{ object.date_fin|date:"d/m" }}</span></p>
|
||||
</div>
|
||||
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom }}</a>
|
||||
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom_complet }}</a>
|
||||
a fait {{ object.type_stage_fem|yesno:"cette,ce" }} <b>{{ object.type_stage_fancy }}</b>
|
||||
{% if object.niveau_scol %}{{ object.niveau_scol_fancy }},{% endif %}
|
||||
{% if object.structure %}au sein de {{ object.structure }}{% endif %}{% if object.encadrants %}, supervisé par {{ object.encadrants }}{% endif %}.</p>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block title %}Espace personnel - ExperiENS{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Bonjour {{ user.profil.nom }} !</h1>
|
||||
<h1>Bonjour {{ user.profil.nom_complet }} !</h1>
|
||||
|
||||
<article>
|
||||
<h2>Mon compte</h2>
|
||||
|
@ -17,20 +17,20 @@
|
|||
{% else %}
|
||||
<h3 class="scolarite">Statut : Archicube</h3>
|
||||
<p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p>
|
||||
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "clipper_login" %}?process=connect">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
|
||||
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "authens:login.cas" %}">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
|
||||
{% endif %}
|
||||
<p><i>Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.</i></p>
|
||||
</section>
|
||||
<section class="profil">
|
||||
<h3>Adresses e-mail</h3>
|
||||
<h3>Adresse e-mail</h3>
|
||||
{% if not user.profil.has_nonENS_email %}<p align="center" class="warning">Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !</p>{% endif %}
|
||||
<p><a href="{% url "account_email" %}">Gérer les adresses e-mail liées à mon compte</a></p>
|
||||
<p><a href="{% url "avisstage:perso" %}">Gérer les adresses e-mail liées à mon compte</a></p>
|
||||
</section>
|
||||
<section class="profil">
|
||||
<h3>Mode de connexion</h3>
|
||||
{% if user.profil.en_scolarite %}<p>En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec l'identifiant {{ user.username }}</p>{% endif %}
|
||||
{% if not user.password %}<p class="warning" align="center">Vous n'avez pas créé de mot de passe interne à ExperiENS. Pensez-y pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
|
||||
<p><a href="{% url "account_change_password" %}">Créer / changer mon mot de passe ExperiENS</a></p>
|
||||
{% if user.profil.en_scolarite %}<p>En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec votre login {{ user.cas_account.cas_login }} et le mot de passe spécifique à ExperiENS que vous aurez défini.</p>{% endif %}
|
||||
{% if not user.password or not user.has_usable_password %}<p class="warning" align="center">Vous n'avez pas créé de mot de passe interne à ExperiENS. Pensez-y pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
|
||||
<p><a href="{% url "authens:reset.pwd" %}">Créer / changer mon mot de passe ExperiENS</a></p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# coding: utf-8
|
||||
|
||||
from allauth.socialaccount.models import SocialAccount
|
||||
from functools import reduce
|
||||
from math import cos, radians, sqrt
|
||||
|
||||
|
@ -18,3 +15,8 @@ def approximate_distance(a, b):
|
|||
dlat = (lat_a - lat_b)
|
||||
distance = 6371000 * sqrt(dlon*dlon + dlat*dlat)
|
||||
return distance
|
||||
|
||||
def is_email_ens(mail, none=False):
|
||||
if mail is None:
|
||||
return none
|
||||
return mail.endswith(".ens.fr") or mail.endswith(".ens.psl.eu")
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
from django_cas_ng.backends import CASBackend
|
||||
from authens.backends import ENSCASBackend as AuthENSBackend
|
||||
from authens.utils import parse_entrance_year
|
||||
|
||||
class ENSCASBackend(CASBackend):
|
||||
def clean_username(self, username):
|
||||
return username.lower().strip()
|
||||
class ENSCASBackend(AuthENSBackend):
|
||||
# Override AuthENS backend user creation to implement the @<promo> logic
|
||||
|
||||
def get_free_username(self, cas_login, attributes):
|
||||
entrance_year = parse_entrance_year(attributes.get("homeDirectory"))
|
||||
if entrance_year is None:
|
||||
return super().get_free_username(cas_login, attributes)
|
||||
entrance_year %= 100
|
||||
return "%s@%02d" % (cas_login, entrance_year)
|
||||
|
|
|
@ -36,14 +36,6 @@ INSTALLED_APPS = [
|
|||
'django_elasticsearch_dsl',
|
||||
|
||||
'widget_tweaks',
|
||||
'allauth_ens',
|
||||
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'allauth.socialaccount',
|
||||
'allauth_cas',
|
||||
|
||||
'allauth_ens.providers.clipper',
|
||||
|
||||
'authens',
|
||||
'tastypie',
|
||||
|
@ -118,24 +110,17 @@ STATIC_URL = '/static/'
|
|||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'authens.backends.ENSCASBackend',
|
||||
'experiENS.auth.ENSCASBackend',
|
||||
)
|
||||
|
||||
CAS_SERVER_URL = "https://cas.eleves.ens.fr/" #SPI CAS
|
||||
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_REDIRECT_URL = reverse_lazy('avisstage:perso')
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||
CAS_FORCE_CHANGE_USERNAME_CASE = "lower"
|
||||
CAS_VERSION = 'CAS_2_SAML_1_0'
|
||||
|
||||
ACCOUNT_ADAPTER = 'avisstage.allauth_adapter.AccountAdapter'
|
||||
SOCIALACCOUNT_ADAPTER = 'avisstage.allauth_adapter.SocialAccountAdapter'
|
||||
AUTHENS_USE_OLDCAS = False
|
||||
|
||||
LOGIN_URL = reverse_lazy('authens:login')
|
||||
LOGOUT_URL = reverse_lazy('authens:logout')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('avisstage:perso')
|
||||
ACCOUNT_HOME_URL = reverse_lazy('avisstage:index')
|
||||
LOGIN_REDIRECT_URL = "/perso/"#reverse_lazy('avisstage:perso')
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
|
|
|
@ -7,7 +7,6 @@ urlpatterns = [
|
|||
|
||||
|
||||
path("authens/", include("authens.urls")),
|
||||
path('account/', include('allauth_ens.urls')),
|
||||
|
||||
path('tinymce/', include('tinymce.urls')),
|
||||
path('taggit_autosuggest/', include('taggit_autosuggest.urls')),
|
||||
|
|
Loading…
Reference in a new issue