Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
7 changed files with 93 additions and 12 deletions
Showing only changes of commit fc08949e67 - Show all commits

View File

@ -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()

View File

@ -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 <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",
)
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, <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_certificate_file = FileField(
label="Certificat (PEM)",

View File

@ -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"

View File

@ -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) }}
<div class="cas_settings">
{{ wtf.form_field(form.cas_ssl_verify) }}
{{ wtf.form_field(form.cas_ssl_certificate_file) }}

View File

@ -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,6 +201,9 @@ 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_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",

View File

@ -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é)"
+ (
"(<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
else "(service CAS non activé)",
else "(service CAS non activé)"
),
"size": 36,
"allow_null": True,
"readonly": not cas_enabled

View File

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