Utilisateurs:

- désactive automatiquement les comptes scodoc7 avec temp=1
 - améliore table export et affichages.
 - améliore log (et préfixe par 'auth: ')
This commit is contained in:
Emmanuel Viennet 2023-03-03 14:33:26 +01:00
parent e65c60873f
commit 6872c20344
7 changed files with 67 additions and 35 deletions

View File

@ -19,7 +19,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
import jwt import jwt
from app import db, log, login from app import db, email, log, login
from app.models import Departement from app.models import Departement
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
@ -85,6 +85,8 @@ class User(UserMixin, db.Model):
date_created = db.Column(db.DateTime, default=datetime.utcnow) date_created = db.Column(db.DateTime, default=datetime.utcnow)
date_expiration = db.Column(db.DateTime, default=None) date_expiration = db.Column(db.DateTime, default=None)
passwd_temp = db.Column(db.Boolean, default=False) passwd_temp = db.Column(db.Boolean, default=False)
"""champ obsolete. Si connexion alors que passwd_temp est vrai,
efface mot de passe et redirige vers accueil."""
token = db.Column(db.Text(), index=True, unique=True) token = db.Column(db.Text(), index=True, unique=True)
token_expiration = db.Column(db.DateTime) token_expiration = db.Column(db.DateTime)
@ -121,7 +123,8 @@ class User(UserMixin, db.Model):
# current_app.logger.info("creating user with roles={}".format(self.roles)) # current_app.logger.info("creating user with roles={}".format(self.roles))
def __repr__(self): def __repr__(self):
return f"<User {self.user_name} id={self.id} dept={self.dept}{' (inactive)' if not self.active else ''}>" return f"""<User {self.user_name} id={self.id} dept={self.dept}{
' (inactive)' if not self.active else ''}>"""
def __str__(self): def __str__(self):
return self.user_name return self.user_name
@ -140,6 +143,19 @@ class User(UserMixin, db.Model):
Returns `True` if the password matched, `False` otherwise. Returns `True` if the password matched, `False` otherwise.
""" """
if not self.active: # inactived users can't login if not self.active: # inactived users can't login
current_app.logger.warning(
f"auth: login attempt from inactive account {self}"
)
return False
if self.passwd_temp:
# Anciens comptes ScoDoc 7 non migrés
# désactive le compte par sécurité.
current_app.logger.warning(f"auth: desactivating legacy account {self}")
self.active = False
self.passwd_temp = True
db.session.add(self)
db.session.commit()
send_notif_desactivation_user(self)
return False return False
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login # if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
@ -158,7 +174,9 @@ class User(UserMixin, db.Model):
def _migrate_scodoc7_password(self, password) -> bool: def _migrate_scodoc7_password(self, password) -> bool:
"""After migration, rehash password.""" """After migration, rehash password."""
if scu.check_scodoc7_password(self.password_scodoc7, password): if scu.check_scodoc7_password(self.password_scodoc7, password):
current_app.logger.warning(f"migrating legacy ScoDoc7 password for {self}") current_app.logger.warning(
f"auth: migrating legacy ScoDoc7 password for {self}"
)
self.set_password(password) self.set_password(password)
self.password_scodoc7 = None self.password_scodoc7 = None
db.session.add(self) db.session.add(self)
@ -182,7 +200,7 @@ class User(UserMixin, db.Model):
token, current_app.config["SECRET_KEY"], algorithms=["HS256"] token, current_app.config["SECRET_KEY"], algorithms=["HS256"]
) )
except jwt.exceptions.ExpiredSignatureError: except jwt.exceptions.ExpiredSignatureError:
log(f"verify_reset_password_token: token expired") log("verify_reset_password_token: token expired")
except: except:
return None return None
try: try:
@ -387,24 +405,17 @@ class User(UserMixin, db.Model):
"""nomplogin est le nom en majuscules suivi du prénom et du login """nomplogin est le nom en majuscules suivi du prénom et du login
e.g. Dupont Pierre (dupont) e.g. Dupont Pierre (dupont)
""" """
if self.nom: nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
n = sco_etud.format_nom(self.nom) return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
else:
n = self.user_name.upper()
return "%s %s (%s)" % (
n,
sco_etud.format_prenom(self.prenom),
self.user_name,
)
@staticmethod @staticmethod
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]: def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
"""Returns id from the string "Dupont Pierre (dupont)" """Returns id from the string "Dupont Pierre (dupont)"
or None if user does not exist or None if user does not exist
""" """
m = re.match(r".*\((.*)\)", nomplogin.strip()) match = re.match(r".*\((.*)\)", nomplogin.strip())
if m: if match:
user_name = m.group(1) user_name = match.group(1)
u = User.query.filter_by(user_name=user_name).first() u = User.query.filter_by(user_name=user_name).first()
if u: if u:
return u.id return u.id
@ -441,6 +452,8 @@ class User(UserMixin, db.Model):
class AnonymousUser(AnonymousUserMixin): class AnonymousUser(AnonymousUserMixin):
"Notre utilisateur anonyme"
def has_permission(self, perm, dept=None): def has_permission(self, perm, dept=None):
return False return False
@ -569,7 +582,7 @@ class UserRole(db.Model):
# maxsplit=1, le dept peut contenir un "_" # maxsplit=1, le dept peut contenir un "_"
if len(fields) != 2: if len(fields) != 2:
current_app.logger.warning( current_app.logger.warning(
f"role_dept_from_string: Invalid role_dept '{role_dept}'" f"auth: role_dept_from_string: Invalid role_dept '{role_dept}'"
) )
raise ScoValueError("Invalid role_dept") raise ScoValueError("Invalid role_dept")
role_name, dept = fields role_name, dept = fields
@ -596,3 +609,23 @@ def get_super_admin():
) )
assert admin_user assert admin_user
return admin_user return admin_user
def send_notif_desactivation_user(user: User):
"""Envoi un message mail de notification à l'admin et à l'adresse du compte désactivé"""
if not user.email:
return
txt = [
f"""Le compte ScoDoc '{user.user_name}' associé à votre adresse <{user.email}>""",
"""a été désactivé par le système car son mot de passe n'était pas valide.\n""",
"""Contactez votre responsable pour le ré-activer.\n""",
"""Ceci est un message automatique, ne pas répondre.""",
]
txt = "\n".join(txt)
email.send_email(
f"ScoDoc: désactivation automatique du compte {user.user_name}",
email.get_from_addr(),
[user.email, current_app.config.get("SCODOC_ADMIN_MAIL")],
txt,
)
return txt

View File

@ -279,16 +279,6 @@ def sco_header(
# Barre menu semestre: # Barre menu semestre:
H.append(formsemestre_page_title(formsemestre_id)) H.append(formsemestre_page_title(formsemestre_id))
# Avertissement si mot de passe à changer
if user_check:
if current_user.passwd_temp:
H.append(
f"""<div class="passwd_warn">
Attention !<br>
Vous avez reçu un mot de passe temporaire.<br>
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
</div>"""
)
# #
if head_message: if head_message:
H.append('<div class="head_message">' + html.escape(head_message) + "</div>") H.append('<div class="head_message">' + html.escape(head_message) + "</div>")

View File

@ -141,8 +141,13 @@ def list_users(
d["_prenom_target"] = target d["_prenom_target"] = target
# Hide passwd modification date (depending on visitor's permission) # Hide passwd modification date (depending on visitor's permission)
if not can_modify: if can_modify:
d["non_migre"] = (
"NON MIGRÉ" if u.passwd_temp or u.password_scodoc7 else "ok"
)
else:
d["date_modif_passwd"] = "(non visible)" d["date_modif_passwd"] = "(non visible)"
d["non_migre"] = ""
columns_ids = [ columns_ids = [
"user_name", "user_name",
@ -153,7 +158,7 @@ def list_users(
"roles_string", "roles_string",
"date_expiration", "date_expiration",
"date_modif_passwd", "date_modif_passwd",
"passwd_temp", "non_migre",
"status_txt", "status_txt",
] ]
# Seul l'admin peut voir les dates de dernière connexion # Seul l'admin peut voir les dates de dernière connexion
@ -182,7 +187,7 @@ def list_users(
"date_expiration": "Expiration", "date_expiration": "Expiration",
"date_modif_passwd": "Modif. mot de passe", "date_modif_passwd": "Modif. mot de passe",
"last_seen": "Dernière cnx.", "last_seen": "Dernière cnx.",
"passwd_temp": "Temp.", "non_migre": "Non migré (!)",
"status_txt": "Etat", "status_txt": "Etat",
"cas_id": "Id CAS", "cas_id": "Id CAS",
"cas_allow_login": "CAS autorisé", "cas_allow_login": "CAS autorisé",

View File

@ -5,7 +5,7 @@
{% macro render_field(field, auth_name=None) %} {% macro render_field(field, auth_name=None) %}
<tr style=""> <tr style="">
{% if auth_name %} {% if auth_name %}
<td class="wtf-field"> {{ field.label }}<span style="font-weight:700;"> ({{ auth_name }}):</span></td> <td class="wtf-field"> {{ field.label }}<span style="font-weight:700;"> (compte {{ auth_name }}):</span></td>
{% else %} {% else %}
<td class="wtf-field">{{ field.label }}</td> <td class="wtf-field">{{ field.label }}</td>
{% endif %} {% endif %}

View File

@ -17,8 +17,12 @@
<b>Nom :</b> {{user.nom or ""}}<br> <b>Nom :</b> {{user.nom or ""}}<br>
<b>Prénom :</b> {{user.prenom or ""}}<br> <b>Prénom :</b> {{user.prenom or ""}}<br>
<b>Mail :</b> {{user.email}}<br> <b>Mail :</b> {{user.email}}<br>
<b>Roles :</b> {{user.get_roles_string()}}<br> <b>Rôles :</b> {{user.get_roles_string()}}<br>
<b>Dept :</b> {{user.dept or ""}} <b>Dept :</b> {{user.dept or ""}}<br>
{% if user.passwd_temp or user.password_scodoc7 %}
<b class="fontred">⚠️ mot de passe invalide (compte ancien non migré à réactiver ou à fermer)</b><br>
{% endif %}
</div> </div>
{% if current_user.is_administrator() %} {% if current_user.is_administrator() %}
<div class="user_info_admin"> <div class="user_info_admin">

View File

@ -83,7 +83,7 @@ class ChangePasswordForm(FlaskForm):
"""formulaire changement mot de passe et mail""" """formulaire changement mot de passe et mail"""
user_name = HiddenField() user_name = HiddenField()
old_password = PasswordField(_l("Identifiez-vous")) old_password = PasswordField(_l("Mot de passe actuel"))
new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur")) new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur"))
bis_password = PasswordField( bis_password = PasswordField(
_l("Répéter"), _l("Répéter"),

View File

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