User: clarifie code vérif. login ScoDoc si CAS, et adapte messages reset passwd

This commit is contained in:
Emmanuel Viennet 2025-01-20 16:22:31 +01:00
parent 60d31a5e6f
commit 2967596de2
7 changed files with 117 additions and 69 deletions

View File

@ -38,6 +38,7 @@ from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$") VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
DEFAULT_RESET_TOKEN_DURATION = 24 * 60 * 60 # seconds (default 24h)
def is_valid_password(cleartxt) -> bool: def is_valid_password(cleartxt) -> bool:
@ -178,6 +179,43 @@ class User(UserMixin, ScoDocModel):
raise ValueError("invalid user_id") raise ValueError("invalid user_id")
return query.first_or_404() if not accept_none else query.first() return query.first_or_404() if not accept_none else query.first()
def can_login_using_scodoc(self) -> bool:
"""True si l'utilisateur peut (essayer de) se connecter avec son compte local ScoDoc
(si par ailleurs un mot de passe valide existe et que le compte est actif)
Toujours vrai pour le super-admin.
Si CAS activé and cas_id renseigné, il faut cas_allow_scodoc_login.
Réglages possibles:
- Global : cas_force CAS forcé pour tous sauf super-admin
- Par utilisateur:
- cas_allow_login : Peut-on se logguer via le CAS ?
- cas_allow_scodoc_login : Si CAS activé, peut-on se logguer sur ScoDoc ?
"""
if self.is_administrator():
return True # super admin ou autorisation individuelle
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
if not cas_enabled:
return True # CAS not enabled
if not self.cas_allow_scodoc_login:
log(
f"""auth: {self.user_name
}: cas enabled, scodoc login not allowed"""
)
return False
if ScoDocSiteConfig.is_cas_forced() and self.cas_id and self.cas_allow_login:
log(
f"""auth: {self.user_name
} (cas_id='{
self.cas_id}'): cas forced and cas_id set: scodoc login not allowed"""
)
return False
return True
def set_password(self, password: str): def set_password(self, password: str):
"Set password" "Set password"
log(f"set_password({self})") log(f"set_password({self})")
@ -197,6 +235,7 @@ class User(UserMixin, ScoDocModel):
def check_password(self, password: str) -> bool: def check_password(self, password: str) -> bool:
"""Check given password vs current one. """Check given password vs current one.
Returns `True` if the password matched, `False` otherwise. Returns `True` if the password matched, `False` otherwise.
Also checks for temporary passwords and if CAS disables scodoc login.
""" """
if not self.active: # inactived users can't login if not self.active: # inactived users can't login
current_app.logger.warning( current_app.logger.warning(
@ -214,28 +253,8 @@ class User(UserMixin, ScoDocModel):
send_notif_desactivation_user(self) send_notif_desactivation_user(self)
return False return False
# if CAS activated and cas_id, allow only super-user and users with cas_allow_scodoc_login if not self.can_login_using_scodoc():
cas_enabled = ScoDocSiteConfig.is_cas_enabled() return False
if cas_enabled and not self.is_administrator():
if not self.cas_allow_scodoc_login:
# CAS activé et compte non autorisé à se logguer sur ScoDoc
log(
f"""auth: login attempt for user {self.user_name}: scodoc login not allowed
"""
)
return False
# si CAS activé et forcé et cas_id renseigné, on ne peut pas se logguer
if (
self.cas_id
and self.cas_allow_login
and ScoDocSiteConfig.get("cas_force")
):
log(
f"""auth: login attempt for user {self.user_name
} (cas_id='{
self.cas_id}'): cas forced and cas_id set: scodoc login not allowed"""
)
return False
if not self.password_hash: # user without password can't login if not self.password_hash: # user without password can't login
if self.password_scodoc7: if self.password_scodoc7:
@ -258,10 +277,16 @@ class User(UserMixin, ScoDocModel):
return True return True
return False return False
def get_reset_password_token(self, expires_in=24 * 60 * 60): def get_reset_password_token(
self, expires_in=DEFAULT_RESET_TOKEN_DURATION
) -> str | None:
"""Un token pour réinitialiser son mot de passe. """Un token pour réinitialiser son mot de passe.
Par défaut valide durant 24 heures. Par défaut valide durant 24 heures.
Note: si le CAS est obligatoire pour l'utilisateur, renvoie None
""" """
# si la config CAS interdit le login ScoDoc, pas de token
if not self.can_login_using_scodoc():
return None
token = jwt.encode( token = jwt.encode(
{"reset_password": self.id, "exp": time() + expires_in}, {"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"], current_app.config["SECRET_KEY"],

View File

@ -87,7 +87,7 @@ def login():
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
if ScoDocSiteConfig.get("cas_force"): if ScoDocSiteConfig.is_cas_forced():
current_app.logger.info("login: forcing CAS") current_app.logger.info("login: forcing CAS")
return redirect(url_for("cas.login")) return redirect(url_for("cas.login"))

View File

@ -1,4 +1,7 @@
<p>Bonjour {{ user.user_name }},</p> <p>Bonjour {{ user.user_name }},</p>
{% if token %}
<p> <p>
Pour réinitialiser votre mot de passe ScoDoc, Pour réinitialiser votre mot de passe ScoDoc,
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}"> <a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
@ -8,6 +11,14 @@
<p>Vous pouvez aussi copier ce lien dans votre navigateur Web:</p> <p>Vous pouvez aussi copier ce lien dans votre navigateur Web:</p>
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p> <p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
{% else %}
<p>Vous ne pouvez pas changer votre mot de passe sur ScoDoc:
en effet, pour vous connecter, vous devez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
</p>
{% endif %}
<p>Si vous n'avez pas demandé à réinitialiser votre mot de passe sur <p>Si vous n'avez pas demandé à réinitialiser votre mot de passe sur
ScoDoc, vous pouvez simplement ignorer ce message. ScoDoc, vous pouvez simplement ignorer ce message.
</p> </p>

View File

@ -1,10 +1,19 @@
Bonjour {{ user.user_name }}, Bonjour {{ user.user_name }},
{% if token %}
Pour réinitialiser votre mot de passe ScoDoc, suivre le lien: Pour réinitialiser votre mot de passe ScoDoc, suivre le lien:
{{ url_for('auth.reset_password', token=token, _external=True) }} {{ url_for('auth.reset_password', token=token, _external=True) }}
{% else %}
Vous ne pouvez pas changer votre mot de passe sur ScoDoc:
en effet, pour vous connecter, vous devez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
{% endif %}
Si vous n'avez pas demandé à réinitialiser votre mot de passe sur Si vous n'avez pas demandé à réinitialiser votre mot de passe sur
ScoDoc, vous pouvez simplement ignorer ce message. ScoDoc, vous pouvez simplement ignorer ce message.

View File

@ -5,20 +5,21 @@
<p> <p>
Votre identifiant ScoDoc est: <b>{{ user.user_name }}</b> Votre identifiant ScoDoc est: <b>{{ user.user_name }}</b>
</p> </p>
{% if cas_force %}
<p>
Pour vous connecter, vous devrez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
</p>
{% endif %}
{% if token %} {% if token %}
<p>Pour initialiser votre mot de passe ScoDoc, <p>Pour initialiser votre mot de passe ScoDoc,
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}"> <a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
cliquez sur ce lien cliquez sur ce lien
</a>. </a>.
</p> </p>
<p>Vous pouvez aussi copier ce lien dans votre navigateur Web:</p> <p>Vous pouvez aussi copier ce lien dans votre navigateur Web:</p>
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p> <p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
<p>Ce lien expirera le {{date_expiration_token}}</p>
{% else %}
<p>Pour vous connecter, vous devrez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
</p>
{% endif %} {% endif %}
<p>A bientôt !</p> <p>A bientôt !</p>

View File

@ -3,16 +3,16 @@ Bienvenue {{ user.prenom }} {{ user.nom }},
Votre accès à ScoDoc vient d'être validé. Votre accès à ScoDoc vient d'être validé.
Votre identifiant ScoDoc est: {{ user.user_name }} Votre identifiant ScoDoc est: {{ user.user_name }}
{% if cas_force %}
<p>
Pour vous connecter, vous devrez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
</p>
{% endif %}
{% if token %} {% if token %}
Pour initialiser votre mot de passe ScoDoc, suivre le lien:
{{ url_for('auth.reset_password', token=token, _external=True) }} Pour initialiser votre mot de passe ScoDoc, suivre le lien:
{{ url_for('auth.reset_password', token=token, _external=True) }}
Ce lien expirera le {{date_expiration_token}}
{% else %}
Pour vous connecter, vous devrez utiliser votre identifiant universitaire
sur le système d'authentification de votre établissement (CAS, ENT).
{% endif %} {% endif %}
A bientôt ! A bientôt !

View File

@ -35,6 +35,7 @@ Emmanuel Viennet, 2021
""" """
import datetime import datetime
import re import re
import time
from enum import auto, IntEnum from enum import auto, IntEnum
from xml.etree import ElementTree from xml.etree import ElementTree
@ -50,11 +51,14 @@ from wtforms.validators import DataRequired, Email, ValidationError, EqualTo
from app import db from app import db
from app import email from app import email
from app.auth.forms import DeactivateUserForm from app.auth.forms import DeactivateUserForm
from app.auth.models import Permission from app.auth.models import (
from app.auth.models import User DEFAULT_RESET_TOKEN_DURATION,
from app.auth.models import Role Permission,
from app.auth.models import UserRole User,
from app.auth.models import is_valid_password Role,
UserRole,
is_valid_password,
)
from app.models import Departement from app.models import Departement
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
@ -64,10 +68,13 @@ from app.decorators import (
permission_required, permission_required,
) )
from app.scodoc import sco_import_users, sco_roles_default from app.scodoc import (
from app.scodoc import sco_users sco_import_users,
from app.scodoc import sco_utils as scu sco_roles_default,
from app.scodoc import sco_xml sco_users,
sco_utils as scu,
sco_xml,
)
from app import log from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied, ScoValueError
from app.scodoc.sco_import_users import generate_password from app.scodoc.sco_import_users import generate_password
@ -779,11 +786,9 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire) # B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
# C: Aucun envoi (mot de passe saisi dans le formulaire) # C: Aucun envoi (mot de passe saisi dans le formulaire)
if vals["welcome"]: # "Envoie un mail d'accueil" coché if vals["welcome"]: # "Envoie un mail d'accueil" coché
if vals["reset_password"] and ( if vals["reset_password"]:
(not ScoDocSiteConfig.get("cas_force")) # nb: le token ne sera envoyé que si le login ScoDoc est autorisé,
or vals.get("cas_allow_scodoc_login", False) # voir get_reset_password_token()
):
# nb: si login scodoc non autorisé car CAS seul, n'envoie pas le mot de passe.
mode = Mode.WELCOME_AND_CHANGE_PASSWORD mode = Mode.WELCOME_AND_CHANGE_PASSWORD
else: else:
mode = Mode.WELCOME_ONLY mode = Mode.WELCOME_ONLY
@ -828,13 +833,10 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
db.session.commit() db.session.commit()
# envoi éventuel d'un message # envoi éventuel d'un message
if mode in (Mode.WELCOME_AND_CHANGE_PASSWORD, Mode.WELCOME_ONLY): if mode in (Mode.WELCOME_AND_CHANGE_PASSWORD, Mode.WELCOME_ONLY):
token = ( token = the_user.get_reset_password_token()
the_user.get_reset_password_token() date_expiration_token = datetime.datetime.fromtimestamp(
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD time.time() + DEFAULT_RESET_TOKEN_DURATION
else None ).strftime(scu.DATEATIME_FMT)
)
cas_force = ScoDocSiteConfig.get("cas_force")
# Le from doit utiliser la préférence du département de l'utilisateur # Le from doit utiliser la préférence du département de l'utilisateur
email.send_email( email.send_email(
"[ScoDoc] Création de votre compte", "[ScoDoc] Création de votre compte",
@ -844,13 +846,13 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"email/welcome.txt", "email/welcome.txt",
user=the_user, user=the_user,
token=token, token=token,
cas_force=cas_force, date_expiration_token=date_expiration_token,
), ),
html_body=render_template( html_body=render_template(
"email/welcome.j2", "email/welcome.j2",
user=the_user, user=the_user,
token=token, token=token,
cas_force=cas_force, date_expiration_token=date_expiration_token,
), ),
) )
flash(f"Mail accueil envoyé à {the_user.email}") flash(f"Mail accueil envoyé à {the_user.email}")