User: modification sémantique réglages CAS, page de login. New pref.

This commit is contained in:
Emmanuel Viennet 2025-01-22 00:30:27 +01:00
parent f98fbd1443
commit a6c162dac8
17 changed files with 151 additions and 109 deletions

View File

@ -105,13 +105,8 @@ def users_info_query():
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
"Vrai si on peut"
if "cas_id" in args and not current_user.has_permission(
Permission.UsersChangeCASId
):
return False, "non autorise a changer cas_id"
if not current_user.is_administrator():
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
if not current_user.has_permission(Permission.UsersChangeCASId):
for field in ("cas_id", "cas_allow_login", "cas_allow_scodoc_login"):
if field in args:
return False, f"non autorise a changer {field}"
return True, ""

View File

@ -158,7 +158,7 @@ CAS_USER_INFO_COMMENTS = (
autorise la connexion via CAS (optionnel, faux par défaut)
""",
"""cas_allow_scodoc_login
autorise connexion via ScoDoc même si CAS activé (optionnel, vrai par défaut)
autorise connexion via ScoDoc même si CAS forcé (optionnel, faux par défaut)
""",
"""email_institutionnel
optionnel, le mail officiel de l'utilisateur.

View File

@ -93,7 +93,7 @@ class User(UserMixin, ScoDocModel):
cas_allow_scodoc_login = db.Column(
db.Boolean, default=False, server_default="false", nullable=False
)
"""Si CAS activé et cas_id renseigné, peut-on se logguer sur ScoDoc directement ?
"""Si CAS activé et forcé, peut-on se logguer sur ScoDoc directement ?
(le rôle ScoSuperAdmin peut toujours, mettre à True pour les utilisateur API)
"""
cas_last_login = db.Column(db.DateTime, nullable=True)
@ -179,42 +179,31 @@ 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_cas(self, require_cas_id=False) -> bool:
"""True si l'utilisateur peut se connecter via CAS.
Attention: si le cas_id est extrait de l'adresse mail, il est au départ vide.
L'argument require_cas_id indique si on le requiert ou pas.
"""
return (
self.cas_allow_login
and (self.cas_id or not require_cas_id)
and ScoDocSiteConfig.is_cas_enabled()
)
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 ?
Toujours vrai pour le super-admin ou si CAS non activé.
Si CAS forcé, il faut cas_allow_scodoc_login.
"""
if self.is_administrator():
return True # super admin ou autorisation individuelle
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
if not cas_enabled:
if not ScoDocSiteConfig.is_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
return self.cas_allow_scodoc_login or not ScoDocSiteConfig.is_cas_forced()
def set_password(self, password: str):
"Set password"

View File

@ -36,7 +36,7 @@ def _login_form():
form = LoginForm()
if form.validate_on_submit():
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
user = (
user: User = (
User.query.filter_by(user_name=form.user_name.data).first()
if is_valid_user_name(form.user_name.data)
else None
@ -51,7 +51,7 @@ def _login_form():
current_app.logger.info("login: success (%s)", form.user_name.data)
if user.passwd_must_be_changed:
if user.passwd_must_be_changed and user.can_login_using_scodoc():
# Mot de passe à changer à la première connexion
dept = user.dept or getattr(g, "scodoc_dept", None)
if not dept:
@ -87,8 +87,10 @@ def login():
if current_user.is_authenticated:
return redirect(url_for("scodoc.index"))
if ScoDocSiteConfig.is_cas_forced():
current_app.logger.info("login: forcing CAS")
if ScoDocSiteConfig.is_cas_forced() and ScoDocSiteConfig.get(
"cas_login_redirect", default=True
):
current_app.logger.info("login: redirecting to CAS login")
return redirect(url_for("cas.login"))
return _login_form()

View File

@ -58,6 +58,10 @@ class ConfigCASForm(FlaskForm):
cas_allow_for_new_users = BooleanField(
"Par défaut, autoriser le CAS aux nouveaux utilisateurs"
)
cas_login_redirect = BooleanField(
"Si le CAS est forcé, redirige immédiatement la page de login vers le CAS",
default=True,
)
cas_server = StringField(
label="URL du serveur CAS",

View File

@ -99,6 +99,9 @@ class ScoDocSiteConfig(models.ScoDocModel):
"user_require_email_institutionnel": bool,
# CAS
"cas_enable": bool,
"cas_force": bool,
"cas_allow_for_new_users": bool,
"cas_login_redirect": bool,
"cas_server": str,
"cas_login_route": str,
"cas_logout_route": str,

View File

@ -653,11 +653,11 @@ class FormSemestre(models.ScoDocModel):
Si le semestre est verrouillé, faux sauf si allow_locked.
"""
user = user or current_user
if user.passwd_must_be_changed or not user.has_permission(
Permission.EditFormSemestre
): # pas chef de dept.
if not user.has_permission(Permission.EditFormSemestre):
# pas chef de dept.
if not self.resp_can_edit or not self.est_responsable(user):
return False
# resp_can_edit et est_responsable
return allow_locked or self.etat
def est_courant(self) -> bool:

View File

@ -62,7 +62,7 @@ _SCO_PERMISSIONS = (
"RelationsEntrepExport",
"Exporter les données de l'application relations entreprises",
),
(1 << 29, "UsersChangeCASId", "Paramétrer l'id CAS"),
(1 << 29, "UsersChangeCASId", "Modifier les paramètres CAS des utilisateurs"),
(1 << 30, "ViewEtudData", "Accéder aux données personnelles des étudiants"),
#
# XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"),

View File

@ -323,31 +323,6 @@ nav li.logout a {
color: rgb(255, 0, 0);
}
div.user_info div {
padding: 8px;
border-radius: 16px;
margin-bottom: 8px;
}
div.user_info ul li {
margin-bottom: 8px;
}
div.user_basics {
border: 1px solid blue;
background-color: #eeeeee;
}
div.user_info_admin {
border: 1px solid red;
background-color: #fdcaca;
}
div.user_info div.permissions {
border: 1px solid rgb(0, 0, 255);
background-color: #dedefd;
}
/* ----- page content ------ */
div.about-logo {

View File

@ -25,7 +25,7 @@
<h1>Modification du compte ScoDoc <tt>{{form.user_name.data}}</tt></h1>
<div class="help" style="margin-top: 32px; margin-bottom: 32px;">
<p>Le mot de passe ScoDoc doit être suffisament complexe.
Il n'a rien à voir avec celui de votre compte ENT (utilisé pour le service CAS).
Il n'a rien à voir avec ceux des comptes utilisés par le service CAS (ENT).
</p>
</div>
<form method="post">

View File

@ -29,7 +29,10 @@ div.small_form {
{% endif %}
{% if is_cas_enabled %}
<div class="cas_else">Sinon vous pouvez vous connecter avec votre compte ScoDoc:</div>
<div class="cas_else">Sinon
{%- if is_cas_forced -%}, si votre compte ScoDoc le permet, {% endif %}
connectez-vous avec vos identifiants ScoDoc:
</div>
{% endif %}
<div class="row {{ 'small_form' if is_cas_enabled else ''}}">
<div class="col-md-4">
@ -38,12 +41,16 @@ div.small_form {
</div>
<div style="margin-top: 32px;">
En cas d'oubli de votre mot de passe ScoDoc (indépendant de CAS)
En cas d'oubli de votre mot de passe ScoDoc
{% if is_cas_enabled %}(indépendant de CAS){% endif %},
<a href="{{ url_for('auth.reset_password_request') }}">cliquez ici pour le réinitialiser</a>.
</div>
<p class="help" style="margin-top: 32px;">L'accès à ScoDoc est strictement réservé aux personnels de
l'établissement. Les étudiants n'y ont pas accès. Pour toute information,
contactez la personne responsable de votre établissement.</p>
<p class="help" style="margin-top: 32px;">
L'accès à ScoDoc est strictement réservé aux personnels de
l'établissement. Les étudiants n'y ont pas accès. Pour toute information,
contactez la personne responsable de votre établissement.
</p>
{% endblock %}

View File

@ -2,52 +2,101 @@
{% extends "base.j2" %}
{% import 'wtf.j2' as wtf %}
{% block styles %}
{{super()}}
<style>
div.ubi div {
}
div.user_info_admin {
border: 1px solid red;
background-color: #fdcaca;
}
div.user_basics {
border: 1px solid blue;
background-color: #eeeeee;
}
div.user_info > div {
padding: 8px;
border-radius: 16px;
margin-bottom: 8px;
}
div.user_info ul li {
margin-bottom: 8px;
}
div.user_info div.permissions {
border: 1px solid rgb(0, 0, 255);
background-color: #dedefd;
}
</style>
{% endblock %}
{% block app_content %}
<div class="user_info">
<h2>Utilisateur: {{user.user_name}} ({{'actif' if user.active else 'fermé'}})</h2>
<div class="user_basics">
<b>Login :</b> {{user.user_name}}
<div><b>Login :</b> {{user.user_name}}
{% if ScoDocSiteConfig.is_cas_enabled() %}
(connexion via ce login ScoDoc
{% if user.can_login_using_scodoc() %}autorisée{% else %}<span class="fontred">interdite</span>
{% endif %})
{% endif -%}
<br>
<b>CAS id:</b> {{user.cas_id or "(aucun)"}}
</div>
<div><b>CAS id:</b> {{user.cas_id or "(aucun)"}}
{% if ScoDocSiteConfig.is_cas_enabled() %}
(CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur)
(CAS {{'autorisé' if user.can_login_using_cas() else 'interdit'}} pour cet utilisateur)
{% if user.can_login_using_scodoc() %}
(connexion sans CAS autorisée)
{% endif %}
{% if user.cas_allow_login and not user.cas_id %}
(pas encore d'identifiant CAS)
{% endif %}
<br>
<b>Nom :</b> {{user.nom or ""}}<br>
<b>Prénom :</b> {{user.prenom or ""}}<br>
{% endif %}
{% if not user.can_login_using_scodoc() and not user.can_login_using_cas() %}
<div class="warning">cet utilisateur ne peut se connecter ni via ScoDoc ni via CAS</div>
{% endif %}
</div>
<div><b>Nom :</b> {{user.nom or ""}}</div>
<div><b>Prénom :</b> {{user.prenom or ""}}</div>
<div>
{% if user.passwd_must_be_changed %}
<div style="color:white; background-color: red; padding:8px; margin-top: 4px; width: fit-content;">mot de passe à changer</div>
<div style="color:white; background-color: red; padding:8px; margin-top: 4px; width: fit-content;">mot de passe ScoDoc à changer</div>
{% endif %}
<b>Mail :</b> {{user.email}}<br>
<b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
<b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
<b>Rôles :</b> {{user.get_roles_string()}}<br>
<b>Dept :</b> {{user.dept or ""}}<br>
</div>
<div><b>Mail :</b> {{user.email}}</div>
<div><b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}</div>
<div><b>Identifiant EDT:</b> {{user.edt_id or ""}}</div>
<div><b>Rôles :</b> {{user.get_roles_string()}}</div>
<div><b>Dept :</b> {{user.dept or ""}}</div>
<div>
{% 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>
<b class="fontred">⚠️ mot de passe invalide (compte ancien non migré à réactiver ou à fermer)</b>
{% endif %}
</div>
</div>
{% if current_user.is_administrator() %}
<div class="user_info_admin">
<b>Dernière vue :</b> {{user.last_seen.strftime(scu.DATEATIME_FMT) if user.last_seen else "-"}}<br>
<b>Dernière connexion CAS :</b> {{user.cas_last_login.strftime(scu.DATEATIME_FMT) if user.cas_last_login else "-"}}<br>
<div class="ubi user_info_admin">
<div>
<b>Dernière vue :</b> {{user.last_seen.strftime(scu.DATEATIME_FMT) if user.last_seen else "-"}}</div><div>
<b>Dernière connexion CAS :</b> {{user.cas_last_login.strftime(scu.DATEATIME_FMT) if user.cas_last_login else "-"}}
</div>
</div>
{% endif %}
<div class="user_basics">
<div class="ubi user_basics">
<div>
<b>Dernière modif mot de passe:</b>
{{user.date_modif_passwd.strftime(scu.DATEATIME_FMT) if user.date_modif_passwd else ""}}<br>
{{user.date_modif_passwd.strftime(scu.DATEATIME_FMT) if user.date_modif_passwd else ""}}
</div>
<div>
<b>Date d'expiration:</b>
{{user.date_expiration.strftime(scu.DATE_FMT) if user.date_expiration else "(sans limite)"}}
</div>
</div>
<div>
@ -79,6 +128,10 @@
}}">{{"désactiver" if user.active else "activer"}} ce compte</a>
</li>
{% endif %}
<li><a class="stdlink" href="
{{url_for('users.index_html', scodoc_dept=g.scodoc_dept)}}
">retour à la liste de tous les utilisateurs</a>
</li>
</ul>
</div>
@ -105,7 +158,7 @@
{# Liste des permissions #}
<div class="permissions">
<p><b>Permissions de cet utilisateur dans le département {{dept}}:</b></p>
<b>Permissions de l'utilisateur {{user.user_name}} dans le département {{dept}}</b>
<ul>
{% for p in Permission.description %}
<li>{{Permission.description[p]}} :

View File

@ -26,6 +26,7 @@
{{ wtf.form_field(form.cas_enable) }}
{{ wtf.form_field(form.cas_force) }}
{{ wtf.form_field(form.cas_allow_for_new_users) }}
{{ wtf.form_field(form.cas_login_redirect) }}
<div class="scobox">
<div class="scobox-title">Routes CAS</div>
{{ wtf.form_field(form.cas_server) }}

View File

@ -282,6 +282,11 @@ def config_cas():
f"""CAS {'' if form.data['cas_allow_for_new_users'] else 'non'
} autorisé par défaut aux nouveaux"""
)
if ScoDocSiteConfig.set("cas_login_redirect", form.data["cas_login_redirect"]):
flash(
f"""Page login {'' if form.data['cas_login_redirect'] else 'non'
} redirigée si CAS forcé"""
)
if ScoDocSiteConfig.set("cas_server", form.data["cas_server"]):
flash("URL du serveur CAS enregistrée")
if ScoDocSiteConfig.set("cas_login_route", form.data["cas_login_route"]):
@ -324,6 +329,7 @@ def config_cas():
form.cas_allow_for_new_users.data = ScoDocSiteConfig.get(
"cas_allow_for_new_users"
)
form.cas_login_redirect.data = ScoDocSiteConfig.get("cas_login_redirect")
form.cas_server.data = ScoDocSiteConfig.get("cas_server")
form.cas_login_route.data = ScoDocSiteConfig.get("cas_login_route")
form.cas_logout_route.data = ScoDocSiteConfig.get("cas_logout_route")

View File

@ -406,6 +406,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
)
]
cas_enabled = ScoDocSiteConfig.is_cas_enabled()
can_edit_cas = current_user.has_permission(Permission.UsersChangeCASId)
if edit:
cas_allow_login_default = the_user.cas_allow_login
else:
@ -453,8 +454,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
),
"size": 36,
"allow_null": True,
"readonly": not cas_enabled
or not current_user.has_permission(Permission.UsersChangeCASId),
"readonly": not cas_enabled or not can_edit_cas,
},
),
(
@ -462,9 +462,8 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{
"title": "Autorise connexion via CAS",
"input_type": "boolcheckbox",
"explanation": """ si CAS est activé.
Seul le super-administrateur peut changer ce réglage.""",
"enabled": current_user.is_administrator(),
"explanation": """ si CAS est activé.""",
"enabled": can_edit_cas,
"default": cas_allow_login_default,
},
),
@ -473,9 +472,8 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{
"title": "Autorise connexion via ScoDoc",
"input_type": "boolcheckbox",
"explanation": """ même si CAS est activé et cas_id renseigné.
Seul le super-administrateur peut changer ce réglage""",
"enabled": current_user.is_administrator(),
"explanation": """ même si CAS est forcé.""",
"enabled": can_edit_cas,
},
),
(
@ -663,7 +661,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
if "obusfacted_u_ser_nam_" in initvalues:
initvalues["user_name"] = initvalues["obusfacted_u_ser_nam_"]
roles = set(vals["roles"]).intersection(editable_roles_strings)
if not current_user.is_administrator():
if not can_edit_cas:
# empeche modification des paramètres CAS
if "cas_allow_login" in vals:
vals["cas_allow_login"] = cas_allow_login_default
@ -672,9 +670,8 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
vals.pop("cas_allow_scodoc_login", None)
else:
vals["cas_allow_scodoc_login"] = the_user.cas_allow_scodoc_login
if not current_user.has_permission(Permission.UsersChangeCASId):
vals.pop("cas_id", None)
if "edit" in vals:
edit = int(vals["edit"])
else:

View File

@ -3,11 +3,21 @@
"Infos sur version ScoDoc"
SCOVERSION = "9.7.56"
SCOVERSION = "9.7.57"
SCONAME = "ScoDoc"
SCONEWS = """
<h4>Année 2024-2025</h4>
<ul>
<li>ScoDoc 9.7</li>
<ul>
<li>Amélioration gestion utilisateurs et CAS</li>
<li>TODO</li>
</ul>
<h4>Année 2023-2024</h4>
<ul>

View File

@ -126,7 +126,7 @@ def test_create_delete(test_client):
def test_edit(test_client):
"test edition object utlisateur"
"test edition object utilisateur"
args = {
"prenom": "No Totoro",
"edt_id": "totorito",