diff --git a/app/auth/models.py b/app/auth/models.py index ea7b3d1b..4b411c35 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta from hashlib import md5 import json import os +import re from time import time from flask import current_app, url_for, g @@ -18,6 +19,7 @@ import jwt from app import db, login +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS import app.scodoc.sco_utils as scu @@ -58,7 +60,7 @@ class User(UserMixin, db.Model): and self.email == current_app.config["SCODOC_ADMIN_MAIL"] ): # super-admin - admin_role = Role.query.filter_by(name="Admin").first() + admin_role = Role.query.filter_by(name="SuperAdmin").first() assert admin_role self.add_role(admin_role, None) db.session.commit() @@ -123,7 +125,7 @@ class User(UserMixin, db.Model): "last_seen": self.last_seen.isoformat() + "Z", "nom": (self.nom or "").encode("utf-8"), # sco8 "prenom": (self.prenom or "").encode("utf-8"), # sco8 - "roles_string": self.get_roles_string(), + "roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info" "user_name": self.user_name.encode("utf-8"), # sco8 } if include_email: @@ -131,11 +133,24 @@ class User(UserMixin, db.Model): return data def from_dict(self, data, new_user=False): - for field in ["user_name", "non", "prenom", "dept", "status", "email"]: + """Set users' attributes from given dict values. + Roles must be encodes as "roles_string", like "Ens_RT, Secr_CJ" + """ + for field in ["nom", "prenom", "dept", "status", "email"]: if field in data: setattr(self, field, data[field]) - if new_user and "password" in data: - self.set_password(data["password"]) + if new_user: + if "user_name" in data: + # never change name of existing users + self.user_name = data["user_name"] + if "password" in data: + self.set_password(data["password"]) + # Roles: roles_string is "Ens_RT, Secr_RT, ..." + if "roles_string" in data: + self.user_roles = [] + for r_d in data["roles_string"].split(","): + role, dept = UserRole.role_dept_from_string(r_d) + self.add_role(role, dept) def get_token(self, expires_in=3600): now = datetime.utcnow() @@ -163,7 +178,7 @@ class User(UserMixin, db.Model): Args: perm: integer, one of the value defined in Permission class. - dept: dept id (eg 'RT') + dept: dept id (eg 'RT'), default to current departement. """ if not self.active: return False @@ -195,20 +210,23 @@ class User(UserMixin, db.Model): self.add_role(role, dept) def set_roles(self, roles, dept): + "set roles in the given dept" self.user_roles = [UserRole(user=self, role=r, dept=dept) for r in roles] def get_roles(self): + "iterator on my roles" for role in self.roles: yield role def get_roles_string(self): """string repr. of user's roles (with depts) - e.g. "EnsRT, EnsInfo, SecrCJ" + e.g. "Ens_RT, Ens_Info, Secr_CJ" """ - return ", ".join("{r.role.name}{r.dept}".format(r=r) for r in self.user_roles) + return ",".join("{r.role.name}_{r.dept}".format(r=r) for r in self.user_roles) def is_administrator(self): - return self.active and self.has_permission(Permission.ScoSuperAdmin, None) + "True if i'm an active SuperAdmin" + return self.active and self.has_permission(Permission.ScoSuperAdmin, dept=None) # Some useful strings: def get_nomplogin(self): @@ -311,6 +329,21 @@ class UserRole(db.Model): def __repr__(self): return "".format(self.user, self.role, self.dept) + @staticmethod + def role_dept_from_string(role_dept): + """Return tuple (role, dept) from the string + role_dept, of the forme "Role_Dept". + role is a Role instance, dept is a string. + """ + fields = role_dept.split("_", 1) # maxsplit=1, le dept peut contenir un "_" + if len(fields) != 2: + raise ScoValueError("Invalid role_dept") + role_name, dept = fields + role = Role.query.filter_by(name=role_name).first() + if role is None: + raise ScoValueError("role %s does not exists" % role_name) + return (role, dept) + @login.user_loader def load_user(id): diff --git a/app/decorators.py b/app/decorators.py index a5a451aa..380ca849 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -51,7 +51,7 @@ class ZRequest(object): self.AUTHENTICATED_USER = current_user self.REMOTE_ADDR = request.remote_addr if request.method == "POST": - self.form = request.form # xxx encode en utf-8 ! + # self.form = request.form # xxx encode en utf-8 ! # Encode en utf-8 pour ScoDoc8 #sco8 self.form = {k: v.encode("utf-8") for (k, v) in request.form.items()} if request.files: @@ -59,6 +59,10 @@ class ZRequest(object): # request.form is a werkzeug.datastructures.ImmutableMultiDict # self.form = self.form.copy() self.form.update(request.files) + # self.cf = request.form.copy() + for k in request.form: + if k.endswith(":list"): + self.form[k[:-5]] = request.form.getlist(k) elif request.method == "GET": # Encode en utf-8 pour ScoDoc8 #sco8 self.form = {k: v.encode("utf-8") for (k, v) in request.args.items()} diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index 00679a67..5c6acd75 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -37,6 +37,7 @@ from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoException from app.scodoc import sco_excel from app.scodoc import sco_preferences +from app.scodoc import sco_users TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept") @@ -112,7 +113,7 @@ def import_users(U, auth_dept="", context=None): created = [] # liste de uid créés try: for u in U: - ok, msg = context._check_modif_user( + ok, msg = sco_users.check_modif_user( 0, user_name=u["user_name"], nom=u["nom"], diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index d43f229e..7a7173f0 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -42,6 +42,7 @@ from flask_login import current_user import cracklib # pylint: disable=import-error +from app import db from app.auth.models import Permission from app.auth.models import User @@ -50,7 +51,6 @@ from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import sco_etud from app.scodoc import sco_excel -from app.scodoc import sco_import_users from app.scodoc import sco_preferences from app.scodoc.gen_tables import GenTable from app.scodoc.notes_log import log @@ -164,7 +164,7 @@ def list_users( users = get_user_list(dept=dept, with_inactives=with_inactives) comm = "dept. %s" % dept.encode(scu.SCO_ENCODING) # sco8 else: - r = get_user_list(with_inactives=with_inactives) + users = get_user_list(with_inactives=with_inactives) comm = "tous" if with_inactives: comm += ", avec anciens" @@ -410,3 +410,62 @@ def user_info_page(context, user_name=None, REQUEST=None): % url_for("users.index_html", scodoc_dept=g.scodoc_dept) ) return scu.sco8_join(H) + F + + +def check_modif_user(edit, user_name="", nom="", prenom="", email="", roles=[]): + """Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1) + Cherche homonymes. + returns (ok, msg) + - ok : si vrai, peut continuer avec ces parametres + (si ok est faux, l'utilisateur peut quand même forcer la creation) + - msg: message warning a presenter l'utilisateur + """ + if not user_name or not nom or not prenom: + return False, "champ requis vide" + if not email: + return False, "vous devriez indiquer le mail de l'utilisateur créé !" + # ce login existe ? + users = _user_list(user_name) + if edit and not users: # safety net, le user_name ne devrait pas changer + return False, "identifiant %s inexistant" % user_name + if not edit and users: + return False, "identifiant %s déjà utilisé" % user_name + + # Des noms/prénoms semblables existent ? + nom = nom.lower().strip() + prenom = prenom.lower().strip() + similar_users = User.query.filter( + User.nom.ilike(nom), User.prenom.ilike(prenom) + ).all() + if edit: + minmatch = 1 + else: + minmatch = 0 + if len(similar_users) > minmatch: + return ( + False, + "des utilisateurs proches existent: " + + ", ".join( + [ + "%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name) + for x in similar_users + ] + ), + ) + # Roles ? + if not roles: + return False, "aucun rôle sélectionné, êtes vous sûr ?" + # ok + return True, "" + + +def user_edit(user_name, vals): + """Edit the user specified by user_name + (ported from Zope to SQLAlchemy, hence strange !) + """ + u = User.query.filter_by(user_name=user_name).first() + if not u: + raise ScoValueError("Invalid user_name") + u.from_dict(vals) + db.session.add(u) + db.session.commit() diff --git a/app/views/users.py b/app/views/users.py index f859bd5b..5551629c 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -33,7 +33,7 @@ Vues s'appuyant sur auth et sco_users Emmanuel Viennet, 2021 """ - +import re import jaxml from flask import g @@ -44,6 +44,8 @@ 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, @@ -57,9 +59,11 @@ 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.sco_exceptions import AccessDenied +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 @@ -100,7 +104,10 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): if edit: if not user_name: raise ValueError("missing argument: user_name") - initvalues = sco_users._user_list(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

") @@ -110,42 +117,67 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): 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() + # 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: - editable_roles = [ - r for r in editable_roles if r.name in {u"Ens", u"Secr", u"Admin"} - ] + # 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" - 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 + 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.union(orig_roles)) - displayed_roles.sort() + 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)): - if displayed_roles[i] not in editable_roles: + for i in range(len(displayed_roles_strings)): + if displayed_roles_strings[i] not in editable_roles_strings: disabled_roles[i] = True - # log('create_user_form: displayed_roles=%s' % displayed_roles) + # stop() # XXX 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 + if current_user.user_name != user_name: + # no one can change its own status descr.append( ( "status", @@ -193,7 +225,7 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): "user_name", {"input_type": "hidden", "default": initvalues["user_name"]}, ), - ("user_id", {"input_type": "hidden", "default": initvalues["user_id"]}), + ("user_name", {"input_type": "hidden", "default": initvalues["user_name"]}), ] descr += [ ( @@ -267,7 +299,8 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): "title": "Rôles", "input_type": "checkbox", "vertical": True, - "allowed_values": displayed_roles, + "labels": displayed_roles_labels, + "allowed_values": displayed_roles_strings, "disabled_items": disabled_roles, }, ), @@ -282,14 +315,15 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): }, ), ] - + # stop() # XXX 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) + set(REQUEST.form["roles"]).union( + orig_roles_strings - editable_roles_strings + ) ) tf = TrivialFormulator( @@ -306,7 +340,7 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): return REQUEST.RESPONSE.redirect(context.UsersURL()) else: vals = tf[2] - roles = set(vals["roles"]).intersection(editable_roles) + roles = set(vals["roles"]).intersection(editable_roles_strings) if REQUEST.form.has_key("edit"): edit = int(REQUEST.form["edit"]) else: @@ -322,7 +356,7 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): user_name = vals["user_name"] # ce login existe ? err = None - users = _user_list(user_name) + 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: @@ -332,7 +366,8 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): return "\n".join(H) + "\n" + tf[1] + F if not force: - ok, msg = context._check_modif_user( + # XXX x = stop() + ok, msg = sco_users.check_modif_user( edit, user_name=user_name, nom=vals["nom"], @@ -350,7 +385,7 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): return "\n".join(H) + "\n" + tf[1] + F - if edit: # modif utilisateur (mais pas passwd) + 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"): @@ -359,24 +394,24 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): 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"): + 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: - if role and not role in editable_roles: + for role in orig_roles_strings: # { "Ens_RT", "Secr_CJ", ... } + if role and not role in editable_roles_strings: roles.add(role) - vals["roles"] = ",".join(roles) + vals["roles_string"] = ",".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) + 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( - "userinfo?user_name=%s&head_message=Utilisateur %s modifié" + "user_info_page?user_name=%s&head_message=Utilisateur %s modifié" % (user_name, user_name) ) else: # creation utilisateur @@ -399,8 +434,18 @@ def create_user_form(context, REQUEST, user_name=None, edit=0): 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) + 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") diff --git a/scodoc_manager.py b/scodoc_manager.py index 5cbadf34..43bb0091 100644 --- a/scodoc_manager.py +++ b/scodoc_manager.py @@ -59,8 +59,8 @@ class ScoDocManager: return self.dept_descriptions[dept_id].db_uri def get_dept_ids(self): - "get (unsorted) dept ids" - return self.dept_descriptions.keys() + "get (sorted) dept ids" + return sorted(self.dept_descriptions.keys()) def get_db_uri(self): """