# -*- 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 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.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_permissions_check import can_handle_passwd from app.scodoc.sco_exceptions import AccessDenied from app.views import users_bp as bp 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") initvalues = sco_users._user_list(user_name) 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 # Noms de roles pouvant etre attribues aux utilisateurs via ce dialogue # si pas SuperAdmin, restreint aux rôles EnsX, SecrX, AdminX # editable_roles = Role.query.all() if is_super_admin: log("create_user_form called by %s (super admin)" % (current_user.user_name,)) else: editable_roles = [ r for r in editable_roles if r.name in {u"Ens", u"Secr", u"Admin"} ] # if not edit: submitlabel = "Créer utilisateur" orig_roles = set() else: submitlabel = "Modifier utilisateur" initvalues["roles"] = initvalues["roles"].split(",") or [] orig_roles = set(initvalues["roles"]) if initvalues["status"] == "old": editable_roles = set() # can't change roles of a disabled user # add existing user roles displayed_roles = list(editable_roles.union(orig_roles)) displayed_roles.sort() disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer for i in range(len(displayed_roles)): if displayed_roles[i] not in editable_roles: disabled_roles[i] = True # log('create_user_form: displayed_roles=%s' % displayed_roles) 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 auth_name != user_name: # no one can't 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_id", {"input_type": "hidden", "default": initvalues["user_id"]}), ] 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, "allowed_values": displayed_roles, "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) # orig_roles - editable_roles REQUEST.form["roles"] = list( set(REQUEST.form["roles"]).union(orig_roles - editable_roles) ) 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(context.UsersURL()) else: vals = tf[2] roles = set(vals["roles"]).intersection(editable_roles) 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 = _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 = context._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) 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 (auth_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: if role and not role in editable_roles: roles.add(role) vals["roles"] = ",".join(roles) # ok, edit log("sco_users: editing %s by %s" % (user_name, auth_name)) # log('sco_users: previous_values=%s' % initvalues) # log('sco_users: new_values=%s' % vals) context._user_edit(user_name, vals) return REQUEST.RESPONSE.redirect( "userinfo?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"], auth_name)) context.create_user(vals, REQUEST=REQUEST) @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