forked from ScoDoc/ScoDoc
479 lines
16 KiB
Python
479 lines
16 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Fonctions sur les utilisateurs
|
|
"""
|
|
|
|
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy
|
|
import re
|
|
|
|
from flask import url_for, g, request
|
|
from flask_login import current_user
|
|
|
|
import cracklib # pylint: disable=import-error
|
|
|
|
from app import db, Departement
|
|
|
|
from app.auth.models import Permission
|
|
from app.auth.models import User
|
|
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc import sco_excel
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc.gen_tables import GenTable
|
|
from app import log
|
|
from app.scodoc.scolog import logdb
|
|
import app.scodoc.sco_utils as scu
|
|
|
|
from app.scodoc.sco_exceptions import (
|
|
AccessDenied,
|
|
ScoValueError,
|
|
)
|
|
|
|
|
|
def is_valid_password(cleartxt):
|
|
"""Check password.
|
|
returns True if OK.
|
|
"""
|
|
if (
|
|
hasattr(scu.CONFIG, "MIN_PASSWORD_LENGTH")
|
|
and scu.CONFIG.MIN_PASSWORD_LENGTH > 0
|
|
and len(cleartxt) < scu.CONFIG.MIN_PASSWORD_LENGTH
|
|
):
|
|
return False # invalid: too short
|
|
try:
|
|
_ = cracklib.FascistCheck(cleartxt)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
# ---------------
|
|
|
|
# ---------------
|
|
|
|
|
|
def index_html(all_depts=False, with_inactives=False, format="html"):
|
|
"gestion utilisateurs..."
|
|
all_depts = int(all_depts)
|
|
with_inactives = int(with_inactives)
|
|
|
|
H = [html_sco_header.html_sem_header("Gestion des utilisateurs")]
|
|
|
|
if current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept):
|
|
H.append(
|
|
'<p><a href="{}" class="stdlink">Ajouter un utilisateur</a>'.format(
|
|
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
|
|
)
|
|
)
|
|
if current_user.is_administrator():
|
|
H.append(
|
|
' <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
|
|
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
|
|
)
|
|
)
|
|
else:
|
|
H.append(
|
|
" Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
|
|
)
|
|
if all_depts:
|
|
checked = "checked"
|
|
else:
|
|
checked = ""
|
|
if with_inactives:
|
|
olds_checked = "checked"
|
|
else:
|
|
olds_checked = ""
|
|
H.append(
|
|
"""<p><form name="f" action="%s" method="get">
|
|
<input type="checkbox" name="all_depts" value="1" onchange="document.f.submit();" %s>Tous les départements</input>
|
|
<input type="checkbox" name="with_inactives" value="1" onchange="document.f.submit();" %s>Avec anciens utilisateurs</input>
|
|
</form></p>"""
|
|
% (request.base_url, checked, olds_checked)
|
|
)
|
|
|
|
L = list_users(
|
|
g.scodoc_dept,
|
|
all_depts=all_depts,
|
|
with_inactives=with_inactives,
|
|
format=format,
|
|
with_links=current_user.has_permission(Permission.ScoUsersAdmin, g.scodoc_dept),
|
|
)
|
|
if format != "html":
|
|
return L
|
|
H.append(L)
|
|
|
|
F = html_sco_header.sco_footer()
|
|
return "\n".join(H) + F
|
|
|
|
|
|
def list_users(
|
|
dept,
|
|
all_depts=False, # tous les departements
|
|
with_inactives=False, # inclut les anciens utilisateurs (status "old")
|
|
format="html",
|
|
with_links=True,
|
|
):
|
|
"List users, returns a table in the specified format"
|
|
from app.scodoc.sco_permissions_check import can_handle_passwd
|
|
|
|
if dept and not all_depts:
|
|
users = get_user_list(dept=dept, with_inactives=with_inactives)
|
|
comm = "dept. %s" % dept
|
|
else:
|
|
users = get_user_list(with_inactives=with_inactives)
|
|
comm = "tous"
|
|
if with_inactives:
|
|
comm += ", avec anciens"
|
|
comm = "(" + comm + ")"
|
|
# -- Add some information and links:
|
|
r = []
|
|
for u in users:
|
|
# Can current user modify this user ?
|
|
can_modify = can_handle_passwd(u, allow_admindepts=True)
|
|
|
|
d = u.to_dict()
|
|
r.append(d)
|
|
# Add links
|
|
if with_links and can_modify:
|
|
target = url_for(
|
|
"users.user_info_page", scodoc_dept=dept, user_name=u.user_name
|
|
)
|
|
d["_user_name_target"] = target
|
|
d["_nom_target"] = target
|
|
d["_prenom_target"] = target
|
|
|
|
# Hide passwd modification date (depending on visitor's permission)
|
|
if not can_modify:
|
|
d["date_modif_passwd"] = "(non visible)"
|
|
|
|
columns_ids = [
|
|
"user_name",
|
|
"nom_fmt",
|
|
"prenom_fmt",
|
|
"email",
|
|
"dept",
|
|
"roles_string",
|
|
"date_expiration",
|
|
"date_modif_passwd",
|
|
"passwd_temp",
|
|
"status_txt",
|
|
]
|
|
# Seul l'admin peut voir les dates de dernière connexion
|
|
if current_user.is_administrator():
|
|
columns_ids.append("last_seen")
|
|
title = "Utilisateurs définis dans ScoDoc"
|
|
tab = GenTable(
|
|
rows=r,
|
|
columns_ids=columns_ids,
|
|
titles={
|
|
"user_name": "Login",
|
|
"nom_fmt": "Nom",
|
|
"prenom_fmt": "Prénom",
|
|
"email": "Mail",
|
|
"dept": "Dept.",
|
|
"roles_string": "Rôles",
|
|
"date_expiration": "Expiration",
|
|
"date_modif_passwd": "Modif. mot de passe",
|
|
"last_seen": "Dernière cnx.",
|
|
"passwd_temp": "Temp.",
|
|
"status_txt": "Etat",
|
|
},
|
|
caption=title,
|
|
page_title="title",
|
|
html_title="""<h2>%d utilisateurs %s</h2>
|
|
<p class="help">Cliquer sur un nom pour changer son mot de passe</p>"""
|
|
% (len(r), comm),
|
|
html_class="table_leftalign list_users",
|
|
html_with_td_classes=True,
|
|
html_sortable=True,
|
|
base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
|
|
pdf_link=False, # table is too wide to fit in a paper page => disable pdf
|
|
preferences=sco_preferences.SemPreferences(),
|
|
)
|
|
|
|
return tab.make_page(format=format, with_html_headers=False)
|
|
|
|
|
|
def get_user_list(dept=None, with_inactives=False):
|
|
"""Returns list of users.
|
|
If dept, select users from this dept,
|
|
else return all users.
|
|
"""
|
|
# was get_userlist
|
|
q = User.query
|
|
if dept is not None:
|
|
q = q.filter_by(dept=dept)
|
|
if not with_inactives:
|
|
q = q.filter_by(active=True)
|
|
return q.order_by(User.nom, User.user_name).all()
|
|
|
|
|
|
def _user_list(user_name):
|
|
"return user as a dict"
|
|
u = User.query.filter_by(user_name=user_name).first()
|
|
if u:
|
|
return u.to_dict()
|
|
else:
|
|
return None
|
|
|
|
|
|
def user_info(user_name_or_id=None, user=None):
|
|
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
|
|
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
|
|
de User.
|
|
"""
|
|
if user_name_or_id is not None:
|
|
if isinstance(user_name_or_id, int):
|
|
u = User.query.filter_by(id=user_name_or_id).first()
|
|
else:
|
|
u = User.query.filter_by(user_name=user_name_or_id).first()
|
|
if u:
|
|
user_name = u.user_name
|
|
info = u.to_dict()
|
|
else:
|
|
info = None
|
|
user_name = "inconnu"
|
|
else:
|
|
info = user.to_dict()
|
|
user_name = user.user_name
|
|
|
|
if not info:
|
|
# special case: user is not in our database
|
|
return {
|
|
"user_name": user_name,
|
|
"nom": user_name,
|
|
"prenom": "",
|
|
"email": "",
|
|
"dept": "",
|
|
"nomprenom": user_name,
|
|
"prenomnom": user_name,
|
|
"prenom_fmt": "",
|
|
"nom_fmt": user_name,
|
|
"nomcomplet": user_name,
|
|
"nomplogin": user_name,
|
|
# "nomnoacc": scu.suppress_accents(user_name),
|
|
"passwd_temp": 0,
|
|
"status": "",
|
|
"date_expiration": None,
|
|
}
|
|
else:
|
|
# Ensure we never publish password hash
|
|
if "password_hash" in info:
|
|
del info["password_hash"]
|
|
return info
|
|
|
|
|
|
def user_info_page(user_name=None):
|
|
"""Display page of info about given user.
|
|
If user_name not specified, user current_user
|
|
"""
|
|
from app.scodoc.sco_permissions_check import can_handle_passwd
|
|
|
|
# peut on divulguer ces infos ?
|
|
if not can_handle_passwd(current_user, allow_admindepts=True):
|
|
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
|
|
|
|
dept = g.scodoc_dept
|
|
if not user_name:
|
|
user = current_user
|
|
else:
|
|
user = User.query.filter_by(user_name=user_name).first()
|
|
if not user:
|
|
raise ScoValueError("invalid user_name")
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title="Utilisateur %s" % user.user_name,
|
|
)
|
|
]
|
|
F = html_sco_header.sco_footer()
|
|
H.append("<h2>Utilisateur: %s" % user.user_name)
|
|
info = user.to_dict()
|
|
if info:
|
|
H.append(" (%(status_txt)s)" % info)
|
|
H.append("</h2>")
|
|
if not info:
|
|
H.append(
|
|
"<p>L' utilisateur '%s' n'est pas défini dans ce module.</p>" % user_name
|
|
)
|
|
if user.has_permission(Permission.ScoEditAllNotes, dept):
|
|
H.append("<p>(il peut modifier toutes les notes de %s)</p>" % dept)
|
|
if user.has_permission(Permission.ScoEditAllEvals, dept):
|
|
H.append("<p>(il peut modifier toutes les évaluations de %s)</p>" % dept)
|
|
if user.has_permission(Permission.ScoImplement, dept):
|
|
H.append("<p>(il peut creer des formations en %s)</p>" % dept)
|
|
else:
|
|
H.append(
|
|
"""<p>
|
|
<b>Login :</b> %(user_name)s<br/>
|
|
<b>Nom :</b> %(nom)s<br/>
|
|
<b>Prénom :</b> %(prenom)s<br/>
|
|
<b>Mail :</b> %(email)s<br/>
|
|
<b>Roles :</b> %(roles_string)s<br/>
|
|
<b>Dept :</b> %(dept)s<br/>
|
|
<b>Dernière modif mot de passe:</b> %(date_modif_passwd)s<br/>
|
|
<b>Date d'expiration:</b> %(date_expiration)s
|
|
<p><ul>
|
|
<li><a class="stdlink" href="form_change_password?user_name=%(user_name)s">changer le mot de passe</a></li>"""
|
|
% info
|
|
)
|
|
if current_user.has_permission(Permission.ScoUsersAdmin, dept):
|
|
H.append(
|
|
f"""
|
|
<li><a class="stdlink" href="{url_for('users.create_user_form', scodoc_dept=g.scodoc_dept,
|
|
user_name=user.user_name, edit=1)}">modifier ce compte</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>
|
|
"""
|
|
% info
|
|
)
|
|
|
|
H.append("</ul>")
|
|
|
|
if current_user.user_name == user_name:
|
|
H.append(
|
|
'<p><b>Se déconnecter: <a class="stdlink" href="%s">logout</a></b></p>'
|
|
% url_for("auth.logout")
|
|
)
|
|
# Liste des permissions
|
|
H.append(
|
|
'<div class="permissions"><p>Permissions de cet utilisateur dans le département %s:</p><ul>'
|
|
% dept
|
|
)
|
|
for p in Permission.description:
|
|
perm = getattr(Permission, p)
|
|
if user.has_permission(perm, dept):
|
|
b = "oui"
|
|
else:
|
|
b = "non"
|
|
H.append("<li>%s : %s</li>" % (Permission.description[p], b))
|
|
H.append("</ul></div>")
|
|
|
|
if current_user.has_permission(Permission.ScoUsersAdmin, dept):
|
|
H.append(
|
|
'<p><a class="stdlink" href="%s">Liste de tous les utilisateurs</a></p>'
|
|
% url_for("users.index_html", scodoc_dept=g.scodoc_dept)
|
|
)
|
|
return "\n".join(H) + F
|
|
|
|
|
|
def check_modif_user(
|
|
edit,
|
|
enforce_optionals=False,
|
|
user_name="",
|
|
nom="",
|
|
prenom="",
|
|
email="",
|
|
dept="",
|
|
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 à presenter à l'utilisateur
|
|
"""
|
|
MSG_OPT = """<br/>Attention: (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
|
|
# ce login existe ?
|
|
user = _user_list(user_name)
|
|
if edit and not user: # safety net, le user_name ne devrait pas changer
|
|
return False, "identifiant %s inexistant" % user_name
|
|
if not edit and user:
|
|
return False, "identifiant %s déjà utilisé" % user_name
|
|
if not user_name or not nom or not prenom:
|
|
return False, "champ requis vide"
|
|
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", user_name):
|
|
return (
|
|
False,
|
|
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
|
|
% user_name,
|
|
)
|
|
if enforce_optionals and len(user_name) > 64:
|
|
return False, "identifiant '%s' trop long (64 caractères)" % user_name
|
|
if enforce_optionals and len(nom) > 64:
|
|
return False, "nom '%s' trop long (64 caractères)" % nom + MSG_OPT
|
|
if enforce_optionals and len(prenom) > 64:
|
|
return False, "prenom '%s' trop long (64 caractères)" % prenom + MSG_OPT
|
|
# check that tha same user_name has not already been described in this import
|
|
if not email:
|
|
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
|
|
if len(email) > 120:
|
|
return False, "email '%s' trop long (120 caractères)" % email
|
|
if not re.fullmatch(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
|
|
return False, "l'adresse mail semble incorrecte"
|
|
# check département
|
|
if (
|
|
enforce_optionals
|
|
and dept != ""
|
|
and Departement.query.filter_by(acronym=dept).first() is None
|
|
):
|
|
return False, "département '%s' inexistant" % dept + MSG_OPT
|
|
if enforce_optionals and not roles:
|
|
return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT
|
|
# ok
|
|
# 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 enforce_optionals and 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
|
|
]
|
|
)
|
|
+ MSG_OPT,
|
|
)
|
|
# Roles ?
|
|
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()
|