Fix: édition des utilisateurs. #305

This commit is contained in:
Emmanuel Viennet 2022-08-26 14:05:25 +02:00
parent cbcff63b35
commit 257248aa2b
5 changed files with 229 additions and 174 deletions

View File

@ -730,11 +730,7 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
R.append("%s</td>" % title) R.append("%s</td>" % title)
R.append('<td class="tf-ro-field%s">' % klass) R.append('<td class="tf-ro-field%s">' % klass)
if ( if input_type in ("text", "text_suggest", "color", "datedmy"):
input_type == "text"
or input_type == "text_suggest"
or input_type == "color"
):
R.append(("%(" + field + ")s") % self.values) R.append(("%(" + field + ")s") % self.values)
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"): elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
if input_type == "boolcheckbox": if input_type == "boolcheckbox":

View File

@ -47,6 +47,15 @@ class ScoValueError(ScoException):
self.dest_url = dest_url self.dest_url = dest_url
class ScoPermissionDenied(ScoValueError):
"""Permission non accordée (appli web)"""
def __init__(self, msg=None, dest_url=None):
if msg is None:
msg = "Opération non autorisée !"
super().__init__(msg, dest_url=dest_url)
class ScoBugCatcher(ScoException): class ScoBugCatcher(ScoException):
"bug avec enquete en cours" "bug avec enquete en cours"

View File

@ -15,8 +15,8 @@ _SCO_PERMISSIONS = (
(1 << 2, "ScoView", "Voir"), (1 << 2, "ScoView", "Voir"),
(1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"), (1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"),
(1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"), (1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"),
(1 << 5, "ScoUsersAdmin", "Gérer les utilisateurs"), (1 << 5, "ScoUsersAdmin", "Gérer les utilisateurs (de son département)"),
(1 << 6, "ScoUsersView", "Voir les utilisateurs"), (1 << 6, "ScoUsersView", "Voir les utilisateurs (de tous les dépts)"),
(1 << 7, "ScoChangePreferences", "Modifier les préférences"), (1 << 7, "ScoChangePreferences", "Modifier les préférences"),
(1 << 8, "ScoChangeFormation", "Changer les formations"), (1 << 8, "ScoChangeFormation", "Changer les formations"),
(1 << 9, "ScoEditFormationTags", "Tagguer les formations"), (1 << 9, "ScoEditFormationTags", "Tagguer les formations"),

View File

@ -18,23 +18,33 @@
{{user.date_expiration.isoformat() if user.date_expiration else "(sans limite)"}} {{user.date_expiration.isoformat() if user.date_expiration else "(sans limite)"}}
<p> <p>
<ul> <ul>
{% if (
current_user.is_administrator()
or current_user.has_permission(Permission.ScoUsersAdmin, user.dept)
) %}
<li><a class="stdlink" href="{{ <li><a class="stdlink" href="{{
url_for( 'users.form_change_password', url_for( 'users.form_change_password',
scodoc_dept=g.scodoc_dept, user_name=user.user_name) scodoc_dept=g.scodoc_dept, user_name=user.user_name)
}}">modifier le mot de passe ou l'adresse mail</a> }}">modifier le mot de passe ou l'adresse mail</a>
</li> </li>
{% endif %}
{% if current_user.has_permission(Permission.ScoUsersAdmin, dept) %} {% if current_user.has_permission(Permission.ScoUsersAdmin, dept) %}
<li><a class="stdlink" href="{{ <li><a class="stdlink" href="{{
url_for('users.create_user_form', scodoc_dept=g.scodoc_dept, url_for('users.create_user_form', scodoc_dept=g.scodoc_dept,
user_name=user.user_name, edit=1) user_name=user.user_name, edit=1)
}}">modifier ce compte</a> }}">modifier ce compte et ses rôles</a>
</li>
<li><a class="stdlink" href="{{
url_for('users.toggle_active_user', scodoc_dept=g.scodoc_dept,
user_name=user.user_name)
}}">{{"désactiver" if user.active else "activer"}} ce compte</a>
</li> </li>
{% endif %} {% endif %}
{% if (
current_user.is_administrator()
or current_user.has_permission(Permission.ScoUsersAdmin, user.dept)
) %}
<li><a class="stdlink" href="{{
url_for('users.toggle_active_user', scodoc_dept=g.scodoc_dept,
user_name=user.user_name)
}}">{{"désactiver" if user.active else "activer"}} ce compte</a>
</li>
{% endif %}
</ul> </ul>
{% if current_user.id == user.id %} {% if current_user.id == user.id %}
@ -45,7 +55,7 @@
{# Liste des permissions #} {# Liste des permissions #}
<div class="permissions"> <div class="permissions">
<p>Permissions de cet utilisateur dans le département {{dept}}:</p> <p><b>Permissions de cet utilisateur dans le département {{dept}}:</b></p>
<ul> <ul>
{% for p in Permission.description %} {% for p in Permission.description %}
<li>{{Permission.description[p]}} : <li>{{Permission.description[p]}} :

View File

@ -68,7 +68,7 @@ from app.scodoc import sco_users
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app import log from app import log
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied, ScoValueError
from app.scodoc.sco_import_users import generate_password from app.scodoc.sco_import_users import generate_password
from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.sco_permissions_check import can_handle_passwd
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@ -141,11 +141,71 @@ def index_html(all_depts=False, with_inactives=False, format="html"):
) )
def _get_administrable_depts() -> list[str]:
"""Liste des acronymes des départements dans lesquels l'utilisateur
courant peut administrer des utilisateurs.
Si SuperAdmin, tous les départements
Sinon, les départements dans lesquels l'utilisateur a la permission ScoUsersAdmin
"""
#
if current_user.is_administrator():
log(f"create_user_form called by {current_user.user_name} (super admin)")
administrable_dept_acronyms = sorted(
[d.acronym for d in Departement.query.all()]
)
else:
administrable_dept_acronyms = current_user.get_depts_with_permission(
Permission.ScoUsersAdmin
)
if None in administrable_dept_acronyms:
administrable_dept_acronyms = sorted(
[d.acronym for d in Departement.query.all()]
)
return administrable_dept_acronyms
def _get_editable_roles(
administrable_dept_acronyms: list = None, all_roles=True
) -> set[tuple[Role, str]]:
"""Rôles modifiables: ensemble de tuples (role, dept_acronym)
( dept_acronym est None si tous dept.)
Si all_roles, tous les rôles définis et modifiables par l'utilisateurs.
Sinon, seulement les rôles "standards" de ScoDoc.
"""
if all_roles:
# tous sauf SuperAdmin
roles = [
r
for r in Role.query.all()
if r.permissions != Permission.ALL_PERMISSIONS[0]
]
else:
# Les rôles standards créés à l'initialisation de ScoDoc:
roles = [
Role.get_named_role(r) for r in sco_roles_default.ROLES_ATTRIBUABLES_DEPT
]
# Génère toutes les combinaisons roles/départements
editable_roles_set = {
(r, dept) for r in roles for dept in administrable_dept_acronyms
}
if current_user.is_administrator():
editable_roles_set |= {
(Role.get_named_role(r), None)
for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC
}
# Un super-admin peut nommer d'autres super-admin:
editable_roles_set |= {(Role.get_named_role("SuperAdmin"), None)}
return editable_roles_set
@bp.route("/create_user_form", methods=["GET", "POST"]) @bp.route("/create_user_form", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoUsersAdmin) @permission_required(Permission.ScoUsersAdmin)
@scodoc7func @scodoc7func
def create_user_form(user_name=None, edit=0, all_roles=False): def create_user_form(user_name=None, edit=0, all_roles=True):
"form. création ou édition utilisateur" "form. création ou édition utilisateur"
if user_name is not None: # scodoc7func converti en int ! if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name) user_name = str(user_name)
@ -162,6 +222,7 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
) )
] ]
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
the_user: User = None
if edit: if edit:
if not user_name: if not user_name:
raise ValueError("missing argument: user_name") raise ValueError("missing argument: user_name")
@ -170,60 +231,6 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
raise ScoValueError("utilisateur inexistant") raise ScoValueError("utilisateur inexistant")
initvalues = the_user.to_dict() initvalues = the_user.to_dict()
H.append(f"<h2>Modification de l'utilisateur {user_name}</h2>") H.append(f"<h2>Modification de l'utilisateur {user_name}</h2>")
else:
H.append("<h2>Création d'un utilisateur</h2>")
is_super_admin = False
if current_user.has_permission(Permission.ScoSuperAdmin, g.scodoc_dept):
H.append("""<p class="warning">Vous êtes super administrateur !</p>""")
is_super_admin = True
if all_roles:
# tous sauf SuperAdmin
standard_roles = [
r
for r in Role.query.all()
if r.permissions != Permission.ALL_PERMISSIONS[0]
]
else:
# Les rôles standards créés à l'initialisation de ScoDoc:
standard_roles = [
Role.get_named_role(r) for r in sco_roles_default.ROLES_ATTRIBUABLES_DEPT
]
# Départements auxquels ont peut associer des rôles via ce dialogue:
# si SuperAdmin, tous les rôles standards dans tous les départements
# sinon, les départements dans lesquels l'utilisateur a la permission ScoUsersAdmin
if is_super_admin:
log(f"create_user_form called by {current_user.user_name} (super admin)")
administrable_dept_acronyms = [d.acronym for d in Departement.query.all()]
else:
# Si on n'est pas SuperAdmin, liste les départements dans lesquels on a la
# permission ScoUsersAdmin
administrable_dept_acronyms = sorted(
set(
[
x.dept or ""
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 administrable_dept_acronyms
}
if current_user.is_administrator():
editable_roles_set |= {
(Role.get_named_role(r), None)
for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC
}
# Un super-admin peut nommer d'autres super-admin:
editable_roles_set |= {(Role.get_named_role("SuperAdmin"), None)}
#
if not edit:
submitlabel = "Créer utilisateur"
orig_roles = set()
else:
submitlabel = "Modifier utilisateur" submitlabel = "Modifier utilisateur"
if "roles_string" in initvalues: if "roles_string" in initvalues:
initvalues["roles"] = initvalues["roles_string"].split(",") initvalues["roles"] = initvalues["roles_string"].split(",")
@ -243,6 +250,26 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
} }
if not initvalues["active"]: if not initvalues["active"]:
editable_roles_set = set() # can't change roles of a disabled user editable_roles_set = set() # can't change roles of a disabled user
else:
H.append("<h2>Création d'un utilisateur</h2>")
submitlabel = "Créer utilisateur"
orig_roles = set()
is_super_admin = current_user.is_administrator()
if is_super_admin:
H.append("""<p class="warning">Vous êtes super administrateur !</p>""")
administrable_dept_acronyms = _get_administrable_depts()
if edit:
if the_user.dept is None: # seul le super admin peut le toucher
edit_only_roles = not current_user.is_administrator()
else:
edit_only_roles = the_user.dept not in administrable_dept_acronyms
else:
edit_only_roles = False # création nouvel utilisateur
editable_roles_set = _get_editable_roles(
administrable_dept_acronyms, all_roles=all_roles
)
editable_roles_strings = { editable_roles_strings = {
r.name + "_" + (dept or "") for (r, dept) in editable_roles_set r.name + "_" + (dept or "") for (r, dept) in editable_roles_set
} }
@ -265,12 +292,29 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
for i, role_string in enumerate(displayed_roles_strings): for i, role_string in enumerate(displayed_roles_strings):
if role_string not in editable_roles_strings: if role_string not in editable_roles_strings:
disabled_roles[i] = True disabled_roles[i] = True
# Formulaire:
descr = [ descr = [
("edit", {"input_type": "hidden", "default": edit}), ("edit", {"input_type": "hidden", "default": edit}),
("nom", {"title": "Nom", "size": 20, "allow_null": False}), (
("prenom", {"title": "Prénom", "size": 20, "allow_null": False}), "nom",
{
"title": "Nom",
"size": 20,
"allow_null": False,
"readonly": edit_only_roles,
},
),
(
"prenom",
{
"title": "Prénom",
"size": 20,
"allow_null": False,
"readonly": edit_only_roles,
},
),
] ]
if current_user.user_name != user_name: if current_user.user_name != user_name and not edit_only_roles:
# no one can change its own status # no one can change its own status
descr.append( descr.append(
( (
@ -316,13 +360,12 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
}, },
), ),
] ]
else: else: # edition: on ne peut pas changer user_name
descr += [ descr += [
( (
"user_name", "user_name",
{"input_type": "hidden", "default": initvalues["user_name"]}, {"input_type": "hidden", "default": initvalues["user_name"]},
), )
("user_name", {"input_type": "hidden", "default": initvalues["user_name"]}),
] ]
descr += [ descr += [
( (
@ -330,9 +373,12 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
{ {
"title": "e-mail", "title": "e-mail",
"input_type": "text", "input_type": "text",
"explanation": "requis, doit fonctionner", "explanation": "requis, doit fonctionner"
if not edit_only_roles
else "",
"size": 20, "size": 20,
"allow_null": False, "allow_null": False,
"readonly": edit_only_roles,
}, },
) )
] ]
@ -362,72 +408,50 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
}, },
), ),
] ]
# Si auth n'a pas de departement (admin global) # Si SuperAdmin, propose de choisir librement le dept du nouvel utilisateur
# propose de choisir librement le dept du nouvel utilisateur selectable_dept_acronyms = set(administrable_dept_acronyms)
# sinon, menu proposant l'ensembe des départements dans lesquels if edit and the_user.dept is not None: # ajoute dept actuel de l'utilisateur
# nous avons la permission ScoUserAdmin + le dept actuel de l'utilisateur selectable_dept_acronyms |= {the_user.dept}
# modifié. if is_super_admin and len(selectable_dept_acronyms) > 1:
if not auth_dept: selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
descr.append( descr.append(
( (
"dept", "dept",
{ {
"title": "Département", "title": "Département",
"input_type": "text", "input_type": "menu",
"size": 12, "explanation": """département de rattachement de l'utilisateur""",
"allow_null": True, "labels": selectable_dept_acronyms,
"explanation": """département de rattachement de l'utilisateur "allowed_values": selectable_dept_acronyms,
(s'il s'agit d'un administrateur, laisser vide si vous voulez "default": g.scodoc_dept
qu'il puisse créer des utilisateurs dans d'autres départements) if g.scodoc_dept in selectable_dept_acronyms
""", else (auth_dept or ""),
}, },
) )
) )
can_choose_dept = True can_choose_dept = True
else: else: # pas de choix de département
selectable_dept_acronyms = set(administrable_dept_acronyms) can_choose_dept = False
if edit and the_user.dept is not None: # ajoute dept actuel de l'utilisateur if edit:
selectable_dept_acronyms |= {the_user.dept}
if len(selectable_dept_acronyms) > 1:
can_choose_dept = True
selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
descr.append( descr.append(
( (
"dept", "d",
{ {
"title": "Département", "input_type": "separator",
"input_type": "menu", "title": f"""L'utilisateur appartient au département {the_user.dept or "(tous)"}""",
"explanation": """département de rattachement de l'utilisateur""",
"labels": selectable_dept_acronyms,
"allowed_values": selectable_dept_acronyms,
"default": g.scodoc_dept
if g.scodoc_dept in selectable_dept_acronyms
else "",
}, },
) )
) )
else: # pas de choix de département else:
can_choose_dept = False descr.append(
if edit: (
descr.append( "d",
( {
"d", "input_type": "separator",
{ "title": f"L'utilisateur sera crée dans le département {auth_dept}",
"input_type": "separator", },
"title": f"L'utilisateur appartient au département {auth_dept}",
},
)
)
else:
descr.append(
(
"d",
{
"input_type": "separator",
"title": f"L'utilisateur sera crée dans le département {auth_dept}",
},
)
) )
)
descr += [ descr += [
( (
@ -435,9 +459,12 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
{ {
"title": "Date d'expiration", # j/m/a "title": "Date d'expiration", # j/m/a
"input_type": "datedmy", "input_type": "datedmy",
"explanation": "j/m/a, laisser vide si pas de limite", "explanation": "j/m/a, laisser vide si pas de limite"
if not edit_only_roles
else "",
"size": 9, "size": 9,
"allow_null": True, "allow_null": True,
"readonly": edit_only_roles,
}, },
), ),
( (
@ -451,17 +478,20 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
"disabled_items": disabled_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 not edit_only_roles:
descr += [
(
"force",
{
"title": "Ignorer les avertissements",
"input_type": "checkbox",
"explanation": "passer outre les avertissements (homonymes, etc)",
"labels": ("",),
"allowed_values": ("1",),
},
),
]
vals = scu.get_request_args() vals = scu.get_request_args()
if "tf_submitted" in vals and "roles" not in vals: if "tf_submitted" in vals and "roles" not in vals:
vals["roles"] = [] vals["roles"] = []
@ -491,7 +521,7 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
else: else:
edit = 0 edit = 0
try: try:
force = int(vals["force"][0]) force = int(vals.get("force", "0")[0])
except (IndexError, ValueError, TypeError): except (IndexError, ValueError, TypeError):
force = 0 force = 0
@ -511,35 +541,37 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
if err: if err:
H.append(tf_error_message(f"""Erreur: {err}""")) H.append(tf_error_message(f"""Erreur: {err}"""))
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
ok, msg = sco_users.check_modif_user(
edit,
enforce_optionals=not force,
user_name=user_name,
nom=vals["nom"],
prenom=vals["prenom"],
email=vals["email"],
dept=vals.get("dept", auth_dept),
roles=vals["roles"],
)
if not ok:
H.append(tf_error_message(msg))
return "\n".join(H) + "\n" + tf[1] + F
if "date_expiration" in vals: if not edit_only_roles:
try: ok_modif, msg = sco_users.check_modif_user(
if vals["date_expiration"]: edit,
vals["date_expiration"] = datetime.datetime.strptime( enforce_optionals=not force,
vals["date_expiration"], "%d/%m/%Y" user_name=user_name,
) nom=vals["nom"],
if vals["date_expiration"] < datetime.datetime.now(): prenom=vals["prenom"],
H.append(tf_error_message("date expiration passée")) email=vals["email"],
return "\n".join(H) + "\n" + tf[1] + F dept=vals.get("dept", auth_dept),
else: roles=vals["roles"],
vals["date_expiration"] = None )
except ValueError: if not ok_modif:
H.append(tf_error_message("date expiration invalide")) H.append(tf_error_message(msg))
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
if "date_expiration" in vals:
try:
if vals["date_expiration"]:
vals["date_expiration"] = datetime.datetime.strptime(
vals["date_expiration"], "%d/%m/%Y"
)
if vals["date_expiration"] < datetime.datetime.now():
H.append(tf_error_message("date expiration passée"))
return "\n".join(H) + "\n" + tf[1] + F
else:
vals["date_expiration"] = None
except ValueError:
H.append(tf_error_message("date expiration invalide"))
return "\n".join(H) + "\n" + tf[1] + F
if edit: # modif utilisateur (mais pas password ni user_name !) if edit: # modif utilisateur (mais pas password ni user_name !)
if (not can_choose_dept) and "dept" in vals: if (not can_choose_dept) and "dept" in vals:
del vals["dept"] del vals["dept"]
@ -566,11 +598,14 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
vals["roles_string"] = ",".join(roles) vals["roles_string"] = ",".join(roles)
# ok, edit # ok, edit
log(f"sco_users: editing {user_name} by {current_user.user_name}") if not edit_only_roles:
log(f"sco_users: previous_values={initvalues}") log(f"sco_users: editing {user_name} by {current_user.user_name}")
log(f"sco_users: new_values={vals}") log(f"sco_users: previous_values={initvalues}")
sco_users.user_edit(user_name, vals) log(f"sco_users: new_values={vals}")
flash(f"Utilisateur {user_name} modifié") sco_users.user_edit(user_name, vals)
flash(f"Utilisateur {user_name} modifié")
else:
sco_users.user_edit(user_name, {"roles_string": vals["roles_string"]})
return flask.redirect( return flask.redirect(
url_for( url_for(
"users.user_info_page", "users.user_info_page",
@ -946,14 +981,19 @@ def toggle_active_user(user_name: str = None):
"""Change active status of a user account""" """Change active status of a user account"""
if user_name is not None: # scodoc7func converti en int ! if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name) user_name = str(user_name)
u = User.query.filter_by(user_name=user_name).first() u = User.query.filter_by(user_name=user_name).first()
if not u: if not u:
raise ScoValueError("invalid user_name") raise ScoValueError("invalid user_name")
# permission check:
if not (
current_user.is_administrator()
or current_user.has_permission(Permission.ScoUsersAdmin, u.dept)
):
raise ScoPermissionDenied()
form = DeactivateUserForm() form = DeactivateUserForm()
if ( if request.method == "POST" and form.cancel.data:
request.method == "POST" and form.cancel.data # if cancel button is clicked, the form.cancel.data will be True
): # if cancel button is clicked, the form.cancel.data will be True
# flash
return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept)) return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
if form.validate_on_submit(): if form.validate_on_submit():
u.active = not u.active u.active = not u.active