diff --git a/app/auth/models.py b/app/auth/models.py index 1022e90b8..e6bebcb85 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -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"], diff --git a/app/auth/routes.py b/app/auth/routes.py index 6adb9a1e8..51c2a939c 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -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")) diff --git a/app/templates/email/reset_password.j2 b/app/templates/email/reset_password.j2 index 9e92a785e..44fcfbf33 100644 --- a/app/templates/email/reset_password.j2 +++ b/app/templates/email/reset_password.j2 @@ -1,4 +1,7 @@ +

Bonjour {{ user.user_name }},

+ +{% if token %}

Pour réinitialiser votre mot de passe ScoDoc, @@ -8,6 +11,14 @@

Vous pouvez aussi copier ce lien dans votre navigateur Web:

{{ 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.

diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt index 2136a52d6..e308c54ea 100644 --- a/app/templates/email/reset_password.txt +++ b/app/templates/email/reset_password.txt @@ -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. diff --git a/app/templates/email/welcome.j2 b/app/templates/email/welcome.j2 index a785479c2..01d045b3e 100644 --- a/app/templates/email/welcome.j2 +++ b/app/templates/email/welcome.j2 @@ -5,20 +5,21 @@

Votre identifiant ScoDoc est: {{ user.user_name }}

-{% if cas_force %} -

- Pour vous connecter, vous devrez utiliser votre identifiant universitaire - sur le système d'authentification de votre établissement (CAS, ENT). -

-{% endif %} + {% if token %} -

Pour initialiser votre mot de passe ScoDoc, - - cliquez sur ce lien - . -

-

Vous pouvez aussi copier ce lien dans votre navigateur Web:

-

{{ url_for('auth.reset_password', token=token, _external=True) }}

+

Pour initialiser votre mot de passe ScoDoc, + + cliquez sur ce lien + . +

+

Vous pouvez aussi copier ce lien dans votre navigateur Web:

+

{{ 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 !

diff --git a/app/templates/email/welcome.txt b/app/templates/email/welcome.txt index dffb870b5..99072d373 100644 --- a/app/templates/email/welcome.txt +++ b/app/templates/email/welcome.txt @@ -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 %} -

- Pour vous connecter, vous devrez utiliser votre identifiant universitaire - sur le système d'authentification de votre établissement (CAS, ENT). -

-{% 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 ! diff --git a/app/views/users.py b/app/views/users.py index dd26a3128..8612d0a8f 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -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}")