forked from ScoDoc/DocScoDoc
595 lines
21 KiB
Python
595 lines
21 KiB
Python
# -*- 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 datetime
|
|
import re
|
|
from xml.etree import ElementTree
|
|
|
|
import flask
|
|
from flask import g
|
|
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.models import Departement
|
|
|
|
from app.decorators import (
|
|
scodoc,
|
|
scodoc7func,
|
|
permission_required,
|
|
)
|
|
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_users
|
|
from app.scodoc import sco_utils as scu
|
|
from app.scodoc import sco_xml
|
|
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
|
|
|
|
|
|
@bp.route("/")
|
|
@bp.route("/index_html")
|
|
@scodoc
|
|
@permission_required(Permission.ScoUsersView)
|
|
@scodoc7func
|
|
def index_html(REQUEST, all_depts=False, with_inactives=False, format="html"):
|
|
return sco_users.index_html(
|
|
REQUEST=REQUEST,
|
|
all_depts=all_depts,
|
|
with_inactives=with_inactives,
|
|
format=format,
|
|
)
|
|
|
|
|
|
@bp.route("/user_info")
|
|
@scodoc
|
|
@permission_required(Permission.ScoUsersView)
|
|
@scodoc7func
|
|
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"])
|
|
@scodoc
|
|
@permission_required(Permission.ScoUsersAdmin)
|
|
@scodoc7func
|
|
def create_user_form(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(bodyOnLoad="init_tf_form('')")]
|
|
F = html_sco_header.sco_footer()
|
|
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("<h2>Modification de l'utilisateur %s</h2>" % user_name)
|
|
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
|
|
|
|
# Les rôles standards créés à l'initialisation de ScoDoc:
|
|
standard_roles = [Role.get_named_role(r) for r in ("Ens", "Secr", "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 = [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
|
|
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"] = []
|
|
if "date_expiration" in initvalues:
|
|
initvalues["date_expiration"] = (
|
|
u.date_expiration.strftime("%d/%m/%Y") if u.date_expiration else ""
|
|
)
|
|
initvalues["status"] = "" if u.active else "old"
|
|
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 flask.redirect(scu.UsersURL())
|
|
else:
|
|
vals = tf[2]
|
|
roles = set(vals["roles"]).intersection(editable_roles_strings)
|
|
if "edit" in REQUEST.form:
|
|
edit = int(REQUEST.form["edit"])
|
|
else:
|
|
edit = 0
|
|
try:
|
|
force = int(vals["force"][0])
|
|
except (IndexError, ValueError, TypeError):
|
|
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 "<em>Ignorer les avertissements</em>" 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 "dept" in vals:
|
|
del vals["dept"]
|
|
if "passwd" in vals:
|
|
del vals["passwd"]
|
|
if "date_modif_passwd" in vals:
|
|
del vals["date_modif_passwd"]
|
|
if "user_name" in vals:
|
|
del vals["user_name"]
|
|
if (current_user.user_name == user_name) and "status" in vals:
|
|
del vals["status"] # no one can't change its own status
|
|
if "date_expiration" in vals:
|
|
try:
|
|
vals["date_expiration"] = datetime.datetime.strptime(
|
|
vals["date_expiration"], "%d/%m/%Y"
|
|
)
|
|
except ValueError:
|
|
H.append(tf_error_message("date expiration invalide"))
|
|
return "\n".join(H) + "\n" + tf[1] + F
|
|
if "status" in vals:
|
|
vals["active"] = vals["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 flask.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 flask.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")
|
|
@scodoc
|
|
@permission_required(Permission.ScoUsersView)
|
|
@scodoc7func
|
|
def user_info_page(user_name, REQUEST=None):
|
|
return sco_users.user_info_page(user_name=user_name, REQUEST=REQUEST)
|
|
|
|
|
|
@bp.route("/get_user_list_xml")
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@scodoc7func
|
|
def get_user_list_xml(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.suppress_accents(start).lower()
|
|
# TODO : à refaire avec une requete SQL #py3
|
|
# (et en json)
|
|
userlist = [
|
|
user
|
|
for user in userlist
|
|
if scu.suppress_accents((user.nom or "").lower()).startswith(start)
|
|
]
|
|
if REQUEST:
|
|
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
|
|
doc = ElementTree.Element("results")
|
|
for user in userlist[:limit]:
|
|
x_rs = ElementTree.Element("rs", id=str(user.id), info="")
|
|
x_rs.text = user.get_nomplogin()
|
|
doc.append(x_rs)
|
|
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
|
|
|
|
|
@bp.route("/form_change_password")
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@scodoc7func
|
|
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(user_check=False)]
|
|
F = html_sco_header.sco_footer()
|
|
# check access
|
|
if not can_handle_passwd(u):
|
|
return (
|
|
"\n".join(H)
|
|
+ "<p>Vous n'avez pas la permission de changer ce mot de passe</p>"
|
|
+ F
|
|
)
|
|
#
|
|
H.append(
|
|
"""<h2>Changement du mot de passe de <font color="red">%(nomplogin)s</font></h2>
|
|
<p>
|
|
<form action="change_password" method="post"><table>
|
|
<tr><td>Nouveau mot de passe:</td><td><input type="password" size="14" name="password"/></td></tr>
|
|
<tr><td>Confirmation: </td><td><input type="password" size="14" name="password2" /></td></tr>
|
|
</table>
|
|
<input type="hidden" value="%(user_name)s" name="user_name">
|
|
<input type="submit" value="Changer">
|
|
</p>
|
|
<p>Vous pouvez aussi: <a class="stdlink" href="reset_password_form?user_name=%(user_name)s">renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur</a>
|
|
"""
|
|
% {"nomplogin": u.get_nomplogin(), "user_name": user_name}
|
|
)
|
|
return "\n".join(H) + F
|
|
|
|
|
|
@bp.route("/change_password", methods=["POST"])
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@scodoc7func
|
|
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()
|
|
# check password
|
|
if password != password2:
|
|
H.append(
|
|
"""<p>Les deux mots de passes saisis sont différents !</p>
|
|
<p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>"""
|
|
% user_name
|
|
)
|
|
else:
|
|
if not sco_users.is_valid_password(password):
|
|
H.append(
|
|
"""<p><b>ce mot de passe n\'est pas assez compliqué !</b><br/>(oui, il faut un mot de passe vraiment compliqué !)</p>
|
|
<p><a href="form_change_password?user_name=%s" class="stdlink">Recommencer</a></p>
|
|
"""
|
|
% 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(
|
|
"<h2>Changement effectué !</h2><p>Ne notez pas ce mot de passe, mais mémorisez le !</p><p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !</p><p><b>Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.</b></p>"
|
|
)
|
|
return (
|
|
"""<?xml version="1.0" encoding="%s"?>
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
<html>
|
|
<head>
|
|
<title>Mot de passe changé</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=%s" />
|
|
<body><h1>Mot de passe changé !</h1>
|
|
"""
|
|
% (scu.SCO_ENCODING, scu.SCO_ENCODING)
|
|
+ "\n".join(H)
|
|
+ '<a href="%s" class="stdlink">Continuer</a></body></html>'
|
|
% scu.ScoURL()
|
|
)
|
|
return html_sco_header.sco_header() + "\n".join(H) + F
|