From fc08949e67265f30c9ce5c937157cad4271adf19 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 23 Sep 2023 09:48:05 +0200 Subject: [PATCH] =?UTF-8?q?CAS:=20calcul=20du=20cas=5Fid=20=C3=A0=20partir?= =?UTF-8?q?=20du=20mail=20via=20regexp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/models.py | 7 +++++++ app/forms/main/config_cas.py | 26 +++++++++++++++++++++++-- app/models/config.py | 37 ++++++++++++++++++++++++++++++++++++ app/templates/config_cas.j2 | 1 + app/views/scodoc.py | 19 ++++++++++++------ app/views/users.py | 13 ++++++++++--- sco_version.py | 2 +- 7 files changed, 93 insertions(+), 12 deletions(-) diff --git a/app/auth/models.py b/app/auth/models.py index a293685ee..073f687e9 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -306,6 +306,13 @@ class User(UserMixin, db.Model): role, dept = UserRole.role_dept_from_string(r_d) self.add_role(role, dept) + # Set cas_id using regexp if configured: + exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp") + if exp and self.email_institutionnel: + cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel) + if cas_id is not None: + self.cas_id = cas_id + def get_token(self, expires_in=3600): "Un jeton pour cet user. Stocké en base, non commité." now = datetime.utcnow() diff --git a/app/forms/main/config_cas.py b/app/forms/main/config_cas.py index f68aa3ccb..7e2b73c56 100644 --- a/app/forms/main/config_cas.py +++ b/app/forms/main/config_cas.py @@ -30,8 +30,17 @@ Formulaire configuration CAS """ from flask_wtf import FlaskForm -from wtforms import BooleanField, SubmitField +from wtforms import BooleanField, SubmitField, ValidationError from wtforms.fields.simple import FileField, StringField +from wtforms.validators import Optional + +from app.models import ScoDocSiteConfig + + +def check_cas_uid_from_mail_regexp(form, field): + "Vérifie la regexp fournie pur l'extraction du CAS id" + if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data): + raise ValidationError("expression régulière invalide") class ConfigCASForm(FlaskForm): @@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm): ) cas_login_route = StringField( label="Route du login CAS", - description="""ajouté à l'URL du serveur: exemple /cas (si commence par /, part de la racine)""", + description="""ajouté à l'URL du serveur: exemple /cas + (si commence par /, part de la racine)""", default="/cas", ) cas_logout_route = StringField( @@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm): comptes utilisateurs.""", ) + cas_uid_from_mail_regexp = StringField( + label="Expression pour extraire l'identifiant utilisateur", + description="""regexp python appliquée au mail institutionnel de l'utilisateur, + dont le premier groupe doit donner l'identifiant CAS. + Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte. + Par exemple, (.*)@ indique que le mail sans le domaine (donc toute + la partie avant le @) est l'identifiant. + Pour prendre le mail complet, utiliser (.*). + """, + validators=[Optional(), check_cas_uid_from_mail_regexp], + ) + cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_certificate_file = FileField( label="Certificat (PEM)", diff --git a/app/models/config.py b/app/models/config.py index 15da815f0..60ce884b4 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -5,6 +5,7 @@ import json import urllib.parse +import re from flask import flash from app import current_app, db, log @@ -103,6 +104,7 @@ class ScoDocSiteConfig(db.Model): "cas_logout_route": str, "cas_validate_route": str, "cas_attribute_id": str, + "cas_uid_from_mail_regexp": str, # Assiduité "morning_time": str, "lunch_time": str, @@ -395,6 +397,41 @@ class ScoDocSiteConfig(db.Model): data_links = json.dumps(links_dict) cls.set("personalized_links", data_links) + @classmethod + def extract_cas_id(cls, email_addr: str) -> str | None: + "Extract cas_id from maill, using regexp in config. None if not possible." + exp = cls.get("cas_uid_from_mail_regexp") + if not exp or not email_addr: + return None + try: + match = re.search(exp, email_addr) + except re.error: + log("error extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + if not match: + log("no match extracting CAS id from '{email_addr}' using regexp '{exp}'") + return None + try: + cas_id = match.group(1) + except IndexError: + log( + "no group found extracting CAS id from '{email_addr}' using regexp '{exp}'" + ) + return None + return cas_id + + @classmethod + def cas_uid_from_mail_regexp_is_valid(cls, exp: str) -> bool: + "True si l'expression régulière semble valide" + # check that it compiles + try: + pattern = re.compile(exp) + except re.error: + return False + # and returns at least one group on a simple cannonical address + match = pattern.search("emmanuel@exemple.fr") + return len(match.groups()) > 0 + @classmethod def assi_get_rounded_time(cls, label: str, default: str) -> float: "Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure" diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2 index ce45cfa67..ca500b1c9 100644 --- a/app/templates/config_cas.j2 +++ b/app/templates/config_cas.j2 @@ -23,6 +23,7 @@ {{ wtf.form_field(form.cas_logout_route) }} {{ wtf.form_field(form.cas_validate_route) }} {{ wtf.form_field(form.cas_attribute_id) }} + {{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
{{ wtf.form_field(form.cas_ssl_verify) }} {{ wtf.form_field(form.cas_ssl_certificate_file) }} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 7891e52b6..c112e04f6 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -173,6 +173,10 @@ def config_cas(): flash("Route de validation CAS enregistrée") if ScoDocSiteConfig.set("cas_attribute_id", form.data["cas_attribute_id"]): flash("Attribut CAS ID enregistré") + if ScoDocSiteConfig.set( + "cas_uid_from_mail_regexp", form.data["cas_uid_from_mail_regexp"] + ): + flash("Expression extraction identifiant CAS enregistrée") if ScoDocSiteConfig.set("cas_ssl_verify", form.data["cas_ssl_verify"]): flash("Vérification SSL modifiée") if form.cas_ssl_certificate_file.data: @@ -197,13 +201,16 @@ def config_cas(): form.cas_logout_route.data = ScoDocSiteConfig.get("cas_logout_route") form.cas_validate_route.data = ScoDocSiteConfig.get("cas_validate_route") form.cas_attribute_id.data = ScoDocSiteConfig.get("cas_attribute_id") - form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") - return render_template( - "config_cas.j2", - form=form, - title="Configuration du Service d'Authentification Central (CAS)", - cas_ssl_certificate_loaded=ScoDocSiteConfig.get("cas_ssl_certificate"), + form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get( + "cas_uid_from_mail_regexp" ) + form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") + return render_template( + "config_cas.j2", + form=form, + title="Configuration du Service d'Authentification Central (CAS)", + cas_ssl_certificate_loaded=ScoDocSiteConfig.get("cas_ssl_certificate"), + ) @bp.route("/ScoDoc/config_assiduites", methods=["GET", "POST"]) diff --git a/app/views/users.py b/app/views/users.py index 6f60b090f..905307604 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -400,9 +400,16 @@ def create_user_form(user_name=None, edit=0, all_roles=True): "title": "Identifiant CAS", "input_type": "text", "explanation": "id du compte utilisateur sur le CAS de l'établissement " - + "(service CAS activé)" - if cas_enabled - else "(service CAS non activé)", + + ( + "(sera déduit de son e-mail institutionnel) " + if ScoDocSiteConfig.get("cas_uid_from_mail_regexp") + else "" + ) + + ( + "(service CAS activé)" + if cas_enabled + else "(service CAS non activé)" + ), "size": 36, "allow_null": True, "readonly": not cas_enabled diff --git a/sco_version.py b/sco_version.py index 48b3bc566..61dc9fa95 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.34" +SCOVERSION = "9.6.35" SCONAME = "ScoDoc"