diff --git a/app/api/users.py b/app/api/users.py index d5cb2f7acae410e9c75464dbe389b1c3962f7d95..9437cebe4b7ceda425e7a491f8da598e42ca6bbc 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -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, "" diff --git a/app/auth/cas.py b/app/auth/cas.py index 5e11c0ac2de5dc9791a5507158bf65ceb39717c5..c3c8cb18439687df1096b4b640bda686ca22f000 100644 --- a/app/auth/cas.py +++ b/app/auth/cas.py @@ -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. diff --git a/app/auth/models.py b/app/auth/models.py index e6bebcb85a2a6740b8e224d31d483a4da30f1e04..7020db177c34b4b7b3d78fc0db032776f12f0eb8 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -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: - 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 + if not ScoDocSiteConfig.is_cas_enabled(): + return True # CAS not enabled - return True + return self.cas_allow_scodoc_login or not ScoDocSiteConfig.is_cas_forced() def set_password(self, password: str): "Set password" diff --git a/app/auth/routes.py b/app/auth/routes.py index 51c2a939cbaced1b409a188785670e6d342bc0bb..d8bae3fc25a746b61f4be6541a7a269e0bb7bd2e 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -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() diff --git a/app/forms/main/config_cas.py b/app/forms/main/config_cas.py index 0d453cf32a6ca74524ee1e263067fc0639d52a68..4c64c8ab573cb37d9de7ca97a2b9f0803f7ae704 100644 --- a/app/forms/main/config_cas.py +++ b/app/forms/main/config_cas.py @@ -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", diff --git a/app/models/config.py b/app/models/config.py index e869a93ac9f074a6ef7c073ead9d817deeafa89e..b8312f39e40fc0259d9042e3ce1ba4ccb51e4eab 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -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, diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index b3f8da2abc31d4986f41f176b6b43dc6662a9bca..988f041d7fa2796a08617bd5b58dc81a1e534204 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -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: diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index dece55410f1077ed92e757c780a70a8665d83bf1..2450b1f54f2c47eed448a7563d2dabba15e31ea5 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -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"), diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index bca1f4e1aec823729d6447929c37d756827c0472..5218567640fb0007946d33955ab10a1a7aa952d3 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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 { diff --git a/app/templates/auth/change_password.j2 b/app/templates/auth/change_password.j2 index ec82dcce5690c83eaccaed9ee10d596d08557288..22a93412fd4b97a3043a4e63e73bc407e46f811e 100644 --- a/app/templates/auth/change_password.j2 +++ b/app/templates/auth/change_password.j2 @@ -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"> diff --git a/app/templates/auth/login.j2 b/app/templates/auth/login.j2 index 10ea1491e54b091870ea249b279341d967de9591..dab260092a11b05113577d3b3590ab7bad4e1e9d 100644 --- a/app/templates/auth/login.j2 +++ b/app/templates/auth/login.j2 @@ -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 %} diff --git a/app/templates/auth/user_info_page.j2 b/app/templates/auth/user_info_page.j2 index 01955929c8edf50df0952ef5295f737777eef135..3083297301c84a02c4cb1a89fb71220b1e1b01cf 100644 --- a/app/templates/auth/user_info_page.j2 +++ b/app/templates/auth/user_info_page.j2 @@ -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 %} + {% 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 %} - <br> - <b>Nom :</b> {{user.nom or ""}}<br> - <b>Prénom :</b> {{user.prenom or ""}}<br> + </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]}} : diff --git a/app/templates/config_cas.j2 b/app/templates/config_cas.j2 index a3bca2e4d94976b982ac96f9c58e022e114fee06..ad17c14761c27e1dc117a1e7680bd939ca6005eb 100644 --- a/app/templates/config_cas.j2 +++ b/app/templates/config_cas.j2 @@ -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) }} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 05980c452556aa65c7e7bb069aaeb4491039f231..8918351d604d9ed9c96096d1322adfdeaf2b0dbb 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -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") diff --git a/app/views/users.py b/app/views/users.py index 472f9bde695d871db2ef102e92333a5f20c493a8..f9ec3dfc64fc1343dfe85a4724502d95a0adb0c6 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -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: diff --git a/sco_version.py b/sco_version.py index df5a1143512d8a3c6300f9462b6295d78c623d30..55d346b734e67c15edda301783d6a58334ee5510 100644 --- a/sco_version.py +++ b/sco_version.py @@ -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> diff --git a/tests/unit/test_users.py b/tests/unit/test_users.py index f947bd3b1103066ac2bdbb01eee4110ffc3992c9..3289f58aded49e7d82b40996bb1c9dbe6514f329 100644 --- a/tests/unit/test_users.py +++ b/tests/unit/test_users.py @@ -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",