diff --git a/app/auth/models.py b/app/auth/models.py
index 1022e90b89e98c2bde45db0097aa6ab74dcef65b..e6bebcb85a2a6740b8e224d31d483a4da30f1e04 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 6adb9a1e8c58b10f05a5b9d51e1dd630171c05a8..51c2a939cbaced1b409a188785670e6d342bc0bb 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 9e92a785e1d0c9b19480492aefbc290c89869b2f..44fcfbf33ab65ef86b5f579d6d50756eea06104b 100644
--- a/app/templates/email/reset_password.j2
+++ b/app/templates/email/reset_password.j2
@@ -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>
diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt
index 2136a52d61908486be034c1c1e395c24d4a79527..e308c54ea1815c3a089f3645cfcbc6a3124d68a3 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 a785479c26cc951342b71e6ff2aaf73e7c9bfcbd..01d045b3e431069d2a806b13b7210fddc0358f59 100644
--- a/app/templates/email/welcome.j2
+++ b/app/templates/email/welcome.j2
@@ -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>
diff --git a/app/templates/email/welcome.txt b/app/templates/email/welcome.txt
index dffb870b54c30e1f5da72d5719a962daddea4336..99072d373f9b320a4276849ef6075a9a8764ac6f 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 %}
-<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 !
diff --git a/app/views/users.py b/app/views/users.py
index dd26a312895cd11b9999cb8f9fefe33952f44d5e..8612d0a8f94032d53ad224b7b8c426d6e273dd61 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}")