CAS: calcul du cas_id à partir du mail via regexp

This commit is contained in:
Emmanuel Viennet 2023-09-23 09:48:05 +02:00 committed by Iziram
parent d869c3d938
commit f2549a24e7
7 changed files with 93 additions and 12 deletions

View File

@ -306,6 +306,13 @@ class User(UserMixin, db.Model):
role, dept = UserRole.role_dept_from_string(r_d) role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept) 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): def get_token(self, expires_in=3600):
"Un jeton pour cet user. Stocké en base, non commité." "Un jeton pour cet user. Stocké en base, non commité."
now = datetime.utcnow() now = datetime.utcnow()

View File

@ -30,8 +30,17 @@ Formulaire configuration CAS
""" """
from flask_wtf import FlaskForm 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.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): class ConfigCASForm(FlaskForm):
@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm):
) )
cas_login_route = StringField( cas_login_route = StringField(
label="Route du login CAS", label="Route du login CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt> (si commence par <tt>/</tt>, part de la racine)""", description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt>
(si commence par <tt>/</tt>, part de la racine)""",
default="/cas", default="/cas",
) )
cas_logout_route = StringField( cas_logout_route = StringField(
@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm):
comptes utilisateurs.""", 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, <tt>(.*)@</tt> indique que le mail sans le domaine (donc toute
la partie avant le <tt>@</tt>) est l'identifiant.
Pour prendre le mail complet, utiliser <tt>(.*)</tt>.
""",
validators=[Optional(), check_cas_uid_from_mail_regexp],
)
cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_verify = BooleanField("Vérification du certificat SSL")
cas_ssl_certificate_file = FileField( cas_ssl_certificate_file = FileField(
label="Certificat (PEM)", label="Certificat (PEM)",

View File

@ -5,6 +5,7 @@
import json import json
import urllib.parse import urllib.parse
import re
from flask import flash from flask import flash
from app import current_app, db, log from app import current_app, db, log
@ -103,6 +104,7 @@ class ScoDocSiteConfig(db.Model):
"cas_logout_route": str, "cas_logout_route": str,
"cas_validate_route": str, "cas_validate_route": str,
"cas_attribute_id": str, "cas_attribute_id": str,
"cas_uid_from_mail_regexp": str,
# Assiduité # Assiduité
"morning_time": str, "morning_time": str,
"lunch_time": str, "lunch_time": str,
@ -395,6 +397,41 @@ class ScoDocSiteConfig(db.Model):
data_links = json.dumps(links_dict) data_links = json.dumps(links_dict)
cls.set("personalized_links", data_links) 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 @classmethod
def assi_get_rounded_time(cls, label: str, default: str) -> float: 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" "Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure"

View File

@ -23,6 +23,7 @@
{{ wtf.form_field(form.cas_logout_route) }} {{ wtf.form_field(form.cas_logout_route) }}
{{ wtf.form_field(form.cas_validate_route) }} {{ wtf.form_field(form.cas_validate_route) }}
{{ wtf.form_field(form.cas_attribute_id) }} {{ wtf.form_field(form.cas_attribute_id) }}
{{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
<div class="cas_settings"> <div class="cas_settings">
{{ wtf.form_field(form.cas_ssl_verify) }} {{ wtf.form_field(form.cas_ssl_verify) }}
{{ wtf.form_field(form.cas_ssl_certificate_file) }} {{ wtf.form_field(form.cas_ssl_certificate_file) }}

View File

@ -173,6 +173,10 @@ def config_cas():
flash("Route de validation CAS enregistrée") flash("Route de validation CAS enregistrée")
if ScoDocSiteConfig.set("cas_attribute_id", form.data["cas_attribute_id"]): if ScoDocSiteConfig.set("cas_attribute_id", form.data["cas_attribute_id"]):
flash("Attribut CAS ID enregistré") 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"]): if ScoDocSiteConfig.set("cas_ssl_verify", form.data["cas_ssl_verify"]):
flash("Vérification SSL modifiée") flash("Vérification SSL modifiée")
if form.cas_ssl_certificate_file.data: if form.cas_ssl_certificate_file.data:
@ -197,6 +201,9 @@ def config_cas():
form.cas_logout_route.data = ScoDocSiteConfig.get("cas_logout_route") form.cas_logout_route.data = ScoDocSiteConfig.get("cas_logout_route")
form.cas_validate_route.data = ScoDocSiteConfig.get("cas_validate_route") form.cas_validate_route.data = ScoDocSiteConfig.get("cas_validate_route")
form.cas_attribute_id.data = ScoDocSiteConfig.get("cas_attribute_id") form.cas_attribute_id.data = ScoDocSiteConfig.get("cas_attribute_id")
form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get(
"cas_uid_from_mail_regexp"
)
form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify")
return render_template( return render_template(
"config_cas.j2", "config_cas.j2",

View File

@ -400,9 +400,16 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"title": "Identifiant CAS", "title": "Identifiant CAS",
"input_type": "text", "input_type": "text",
"explanation": "id du compte utilisateur sur le CAS de l'établissement " "explanation": "id du compte utilisateur sur le CAS de l'établissement "
+ "(service CAS activé)" + (
"(<b>sera déduit de son e-mail institutionnel</b>) "
if ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
else ""
)
+ (
"(service CAS activé)"
if cas_enabled if cas_enabled
else "(service CAS non activé)", else "(service CAS non activé)"
),
"size": 36, "size": 36,
"allow_null": True, "allow_null": True,
"readonly": not cas_enabled "readonly": not cas_enabled

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.34" SCOVERSION = "9.6.35"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"