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
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:
@ -178,6 +179,43 @@ class User(UserMixin, ScoDocModel):
raise ValueError("invalid user_id")
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):
"Set password"
log(f"set_password({self})")
@ -197,6 +235,7 @@ class User(UserMixin, ScoDocModel):
def check_password(self, password: str) -> bool:
"""Check given password vs current one.
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
current_app.logger.warning(
@ -214,28 +253,8 @@ class User(UserMixin, ScoDocModel):
send_notif_desactivation_user(self)
return False
# if CAS activated and cas_id, allow only super-user and users with cas_allow_scodoc_login
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
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.can_login_using_scodoc():
return False
if not self.password_hash: # user without password can't login
if self.password_scodoc7:
@ -258,10 +277,16 @@ class User(UserMixin, ScoDocModel):
return True
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.
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(
{"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"],

View File

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

View File

@ -1,4 +1,7 @@
<p>Bonjour {{ user.user_name }},</p>
{% if token %}
<p>
Pour réinitialiser votre mot de passe ScoDoc,
<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>{{ 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
ScoDoc, vous pouvez simplement ignorer ce message.
</p>

View File

@ -1,10 +1,19 @@
Bonjour {{ user.user_name }},
{% if token %}
Pour réinitialiser votre mot de passe ScoDoc, suivre le lien:
{{ 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
ScoDoc, vous pouvez simplement ignorer ce message.

View File

@ -5,20 +5,21 @@
<p>
Votre identifiant ScoDoc est: <b>{{ user.user_name }}</b>
</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 %}
<p>Pour initialiser votre mot de passe ScoDoc,
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
cliquez sur ce lien
</a>.
</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>Pour initialiser votre mot de passe ScoDoc,
<a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">
cliquez sur ce lien
</a>.
</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>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 %}
<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 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 %}
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 %}
A bientôt !

View File

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