# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # ScoDoc # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """ Module users: interface gestion utilisateurs ré-écriture pour Flask ScoDoc7 / ZScoUsers.py Vues s'appuyant sur auth et sco_users Emmanuel Viennet, 2021 """ import re import jaxml from flask import g from flask import current_app, request from flask_login import current_user from app import db 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.decorators import ( scodoc7func, ScoDoc7Context, permission_required, admin_required, login_required, ZRequest, ) from app.scodoc import html_sco_header from app.scodoc import sco_users from app.scodoc import sco_utils as scu from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.views import users_bp as bp from scodoc_manager import sco_mgr context = ScoDoc7Context("users") # sco8 @bp.route("/") @bp.route("/index_html") @permission_required(Permission.ScoUsersView) @scodoc7func(context) def index_html(context, REQUEST, all_depts=False, with_inactives=False, format="html"): return sco_users.index_html( context, REQUEST=REQUEST, all_depts=all_depts, with_inactives=with_inactives, format=format, ) @bp.route("/user_info") @permission_required(Permission.ScoUsersView) @scodoc7func(context) def user_info(user_name, format="json", REQUEST=None): info = sco_users.user_info(user_name=user_name) return scu.sendResult(REQUEST, info, name="user", format=format) @bp.route("/create_user_form", methods=["GET", "POST"]) @permission_required(Permission.ScoUsersAdmin) @scodoc7func(context) def create_user_form(context, REQUEST, user_name=None, edit=0): "form. creation ou edit utilisateur" auth_dept = current_user.dept initvalues = {} edit = int(edit) H = [html_sco_header.sco_header(context, REQUEST, bodyOnLoad="init_tf_form('')")] F = html_sco_header.sco_footer(context, REQUEST) if edit: if not user_name: raise ValueError("missing argument: user_name") u = User.query.filter_by(user_name=user_name).first() if not u: raise ScoValueError("utilisateur inexistant") initvalues = u.to_dict() H.append("

Modification de l'utilisateur %s

" % user_name) else: H.append("

Création d'un utilisateur

") is_super_admin = False if current_user.has_permission(Permission.ScoSuperAdmin, g.scodoc_dept): H.append("""

Vous êtes super administrateur !

""") is_super_admin = True # Les rôles standards créés à l'initialisation de ScoDoc: standard_roles = [Role.get_named_role(r) for r in (u"Ens", u"Secr", u"Admin")] # Rôles pouvant etre attribués aux utilisateurs via ce dialogue: # si SuperAdmin, tous les rôles standards dans tous les départements # sinon, les départements dans lesquels l'utilisateur a le droit if is_super_admin: log("create_user_form called by %s (super admin)" % (current_user.user_name,)) dept_ids = sco_mgr.get_dept_ids() else: # Si on n'est pas SuperAdmin, liste les départements dans lesquels on a la # permission ScoUsersAdmin dept_ids = sorted( set( [ x.dept for x in UserRole.query.filter_by(user=current_user) if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept ] ) ) editable_roles_set = {(r, dept) for r in standard_roles for dept in dept_ids} # if not edit: submitlabel = "Créer utilisateur" orig_roles = set() else: submitlabel = "Modifier utilisateur" if "roles_string" in initvalues: initvalues["roles"] = initvalues["roles_string"].split(",") else: initvalues["roles"] = [] orig_roles = { # set des roles existants avant édition UserRole.role_dept_from_string(role_dept) for role_dept in initvalues["roles"] } if not initvalues["active"]: editable_roles_set = set() # can't change roles of a disabled user editable_roles_strings = {r.name + "_" + dept for (r, dept) in editable_roles_set} orig_roles_strings = {r.name + "_" + dept for (r, dept) in orig_roles} # add existing user roles displayed_roles = list(editable_roles_set.union(orig_roles)) displayed_roles.sort(key=lambda x: (x[1], x[0].name)) displayed_roles_strings = [r.name + "_" + dept for (r, dept) in displayed_roles] displayed_roles_labels = [ "{dept}: {r.name}".format(dept=dept, r=r) for (r, dept) in displayed_roles ] disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer for i in range(len(displayed_roles_strings)): if displayed_roles_strings[i] not in editable_roles_strings: disabled_roles[i] = True descr = [ ("edit", {"input_type": "hidden", "default": edit}), ("nom", {"title": "Nom", "size": 20, "allow_null": False}), ("prenom", {"title": "Prénom", "size": 20, "allow_null": False}), ] if current_user.user_name != user_name: # no one can change its own status descr.append( ( "status", { "title": "Statut", "input_type": "radio", "labels": ("actif", "ancien"), "allowed_values": ("", "old"), }, ) ) if not edit: descr += [ ( "user_name", { "title": "Pseudo (login)", "size": 20, "allow_null": False, "explanation": "nom utilisé pour la connexion. Doit être unique parmi tous les utilisateurs.", }, ), ( "passwd", { "title": "Mot de passe", "input_type": "password", "size": 14, "allow_null": False, }, ), ( "passwd2", { "title": "Confirmer mot de passe", "input_type": "password", "size": 14, "allow_null": False, }, ), ] else: descr += [ ( "user_name", {"input_type": "hidden", "default": initvalues["user_name"]}, ), ("user_name", {"input_type": "hidden", "default": initvalues["user_name"]}), ] descr += [ ( "email", { "title": "e-mail", "input_type": "text", "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur", "size": 20, "allow_null": True, }, ) ] if not auth_dept: # si auth n'a pas de departement (admin global) # propose de choisir le dept du nouvel utilisateur # sinon, il sera créé dans le même département que auth descr.append( ( "dept", { "title": "Département", "input_type": "text", "size": 12, "allow_null": True, "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""", }, ) ) can_choose_dept = True else: can_choose_dept = False if edit: descr.append( ( "d", { "input_type": "separator", "title": "L'utilisateur appartient au département %s" % auth_dept, }, ) ) else: descr.append( ( "d", { "input_type": "separator", "title": "L'utilisateur sera crée dans le département %s" % auth_dept, }, ) ) descr += [ ( "date_expiration", { "title": "Date d'expiration", # j/m/a "input_type": "date", "explanation": "j/m/a, laisser vide si pas de limite", "size": 9, "allow_null": True, }, ), ( "roles", { "title": "Rôles", "input_type": "checkbox", "vertical": True, "labels": displayed_roles_labels, "allowed_values": displayed_roles_strings, "disabled_items": disabled_roles, }, ), ( "force", { "title": "Ignorer les avertissements", "input_type": "checkbox", "explanation": "passer outre les avertissements (homonymes, etc)", "labels": ("",), "allowed_values": ("1",), }, ), ] if "tf-submitted" in REQUEST.form and not "roles" in REQUEST.form: REQUEST.form["roles"] = [] if "tf-submitted" in REQUEST.form: # Ajoute roles existants mais non modifiables (disabled dans le form) REQUEST.form["roles"] = list( set(REQUEST.form["roles"]).union( orig_roles_strings - editable_roles_strings ) ) tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, descr, initvalues=initvalues, submitlabel=submitlabel, cancelbutton="Annuler", ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] + F elif tf[0] == -1: return REQUEST.RESPONSE.redirect(scu.UsersURL()) else: vals = tf[2] roles = set(vals["roles"]).intersection(editable_roles_strings) if REQUEST.form.has_key("edit"): edit = int(REQUEST.form["edit"]) else: edit = 0 try: force = int(vals["force"][0]) except: force = 0 if edit: user_name = initvalues["user_name"] else: user_name = vals["user_name"] # ce login existe ? err = None users = sco_users._user_list(user_name) if edit and not users: # safety net, le user_name ne devrait pas changer err = "identifiant %s inexistant" % user_name if not edit and users: err = "identifiant %s déjà utilisé" % user_name if err: H.append(tf_error_message("""Erreur: %s""" % err)) return "\n".join(H) + "\n" + tf[1] + F if not force: ok, msg = sco_users.check_modif_user( edit, user_name=user_name, nom=vals["nom"], prenom=vals["prenom"], email=vals["email"], roles=vals["roles"], ) if not ok: H.append( tf_error_message( """Attention: %s (vous pouvez forcer l'opération en cochant "Ignorer les avertissements" en bas de page)""" % msg ) ) return "\n".join(H) + "\n" + tf[1] + F if edit: # modif utilisateur (mais pas passwd ni user_name !) if (not can_choose_dept) and vals.has_key("dept"): del vals["dept"] if vals.has_key("passwd"): del vals["passwd"] if vals.has_key("date_modif_passwd"): del vals["date_modif_passwd"] if vals.has_key("user_name"): del vals["user_name"] if (current_user.user_name == user_name) and vals.has_key("status"): del vals["status"] # no one can't change its own status # traitement des roles: ne doit pas affecter les roles # que l'on en controle pas: for role in orig_roles_strings: # { "Ens_RT", "Secr_CJ", ... } if role and not role in editable_roles_strings: roles.add(role) vals["roles_string"] = ",".join(roles) # ok, edit log("sco_users: editing %s by %s" % (user_name, current_user.user_name)) log("sco_users: previous_values=%s" % initvalues) log("sco_users: new_values=%s" % vals) sco_users.user_edit(user_name, vals) return REQUEST.RESPONSE.redirect( "user_info_page?user_name=%s&head_message=Utilisateur %s modifié" % (user_name, user_name) ) else: # creation utilisateur vals["roles"] = ",".join(vals["roles"]) # check identifiant if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]+$", vals["user_name"]): msg = tf_error_message( "identifiant invalide (pas d'accents ni de caractères spéciaux)" ) return "\n".join(H) + msg + "\n" + tf[1] + F # check passwords if vals["passwd"] != vals["passwd2"]: msg = tf_error_message( """Les deux mots de passes ne correspondent pas !""" ) return "\n".join(H) + msg + "\n" + tf[1] + F if not sco_users.is_valid_password(vals["passwd"]): msg = tf_error_message("""Mot de passe trop simple, recommencez !""") return "\n".join(H) + msg + "\n" + tf[1] + F if not can_choose_dept: vals["dept"] = auth_dept # ok, go log( "sco_users: new_user %s by %s" % (vals["user_name"], current_user.user_name) ) u = User() u.from_dict(vals, new_user=True) db.session.add(u) db.session.commit() return REQUEST.RESPONSE.redirect( "user_info_page?user_name=%s&head_message=Nouvel utilisateur créé" % (user_name) ) @bp.route("/import_users_form") def import_users_form(): raise NotImplementedError() @bp.route("/user_info_page") @permission_required(Permission.ScoUsersView) @scodoc7func(context) def user_info_page(user_name, REQUEST=None): return sco_users.user_info_page(context, user_name=user_name, REQUEST=REQUEST) @bp.route("/get_user_list_xml") @permission_required(Permission.ScoView) @scodoc7func(context) def get_user_list_xml(context, dept=None, start="", limit=25, REQUEST=None): """Returns XML list of users with name (nomplogin) starting with start. Used for forms auto-completion.""" userlist = sco_users.get_user_list(dept=dept) start = scu.suppression_diacritics(unicode(start, "utf-8")) start = scu.strlower(str(start)) # TODO : à refaire avec une requete SQL #py3 # (et en json) userlist = [ user for user in userlist if scu.suppress_accents(scu.strlower(user.nom)).startswith(start) ] if REQUEST: REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) doc.results() for user in userlist[:limit]: doc._push() doc.rs(user["nomplogin"], id=user["user_id"], info="") doc._pop() return repr(doc) @bp.route("/form_change_password") @permission_required(Permission.ScoView) @scodoc7func(context) def form_change_password(REQUEST, user_name=None): """Formulaire de changement mot de passe de l'utilisateur user_name. Un utilisateur peut toujours changer son propre mot de passe. """ if not user_name: u = current_user else: u = User.query.filter_by(user_name=user_name).first() H = [html_sco_header.sco_header(context, REQUEST, user_check=False)] F = html_sco_header.sco_footer(context, REQUEST) # check access if not can_handle_passwd(u): return ( "\n".join(H) + "

Vous n'avez pas la permission de changer ce mot de passe

" + F ) # H.append( """

Changement du mot de passe de %(nomplogin)s

Nouveau mot de passe:
Confirmation:

Vous pouvez aussi: renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur """ % {"nomplogin": u.get_nomplogin(), "user_name": user_name} ) return "\n".join(H) + F @bp.route("/change_password", methods=["POST"]) @permission_required(Permission.ScoView) @scodoc7func(context) def change_password(user_name, password, password2, REQUEST): "Change the password for user given by user_name" u = User.query.filter_by(user_name=user_name).first() # Check access permission if not can_handle_passwd(u): # access denied log( "change_password: access denied (authuser=%s, user_name=%s, ip=%s)" % (REQUEST.AUTHENTICATED_USER, user_name, REQUEST.REMOTE_ADDR) ) raise AccessDenied("vous n'avez pas la permission de changer ce mot de passe") H = [] F = html_sco_header.sco_footer(context, REQUEST) # check password if password != password2: H.append( """

Les deux mots de passes saisis sont différents !

Recommencer

""" % user_name ) else: if not sco_users.is_valid_password(password): H.append( """

ce mot de passe n\'est pas assez compliqué !
(oui, il faut un mot de passe vraiment compliqué !)

Recommencer

""" % user_name ) else: # ok, strong password db.session.add(u) u.set_password(password) db.session.commit() # # ici page simplifiee car on peut ne plus avoir # le droit d'acceder aux feuilles de style H.append( "

Changement effectué !

Ne notez pas ce mot de passe, mais mémorisez le !

Rappel: il est interdit de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !

Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.

" ) return ( """ Mot de passe changé

Mot de passe changé !

""" % (scu.SCO_ENCODING, scu.SCO_ENCODING) + "\n".join(H) + 'Continuer' % scu.ScoURL() ) return html_sco_header.sco_header(context, REQUEST) + "\n".join(H) + F @bp.route("/delete_user_form", methods=["GET", "POST"]) @permission_required(Permission.ScoUsersAdmin) @scodoc7func(context) def delete_user_form(REQUEST, user_name, dialog_confirmed=False): "delete user" u = User.query.filter_by(user_name=user_name).first() # Check access permission if not can_handle_passwd(u): # access denied (or non existent user) return ( html_sco_header.sco_header(context, REQUEST, user_check=False) + "

Vous n'avez pas la permission de supprimer cet utilisateur

" + html_sco_header.sco_footer(context, REQUEST) ) if not dialog_confirmed: return scu.confirm_dialog( context, """

Confirmer la suppression de l\'utilisateur %s ?

En général, il est déconseillé de supprimer un utilisateur, son identité étant référencé dans les modules de formation. N'utilisez cette fonction qu'en cas d'erreur (création de doublons, etc).

""" % user_name, dest_url="", REQUEST=REQUEST, cancel_url=scu.UsersURL(), parameters={"user_name": user_name}, ) db.session.delete(u) db.session.commit() return REQUEST.RESPONSE.redirect( scu.UsersURL() + r"?head_message=Utilisateur%20supprimé" )