forked from ScoDoc/ScoDoc
572 lines
18 KiB
Python
572 lines
18 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""
|
|
ScoDoc 9 API : accès aux utilisateurs
|
|
|
|
CATEGORY
|
|
--------
|
|
Utilisateurs
|
|
"""
|
|
|
|
from flask import g, request
|
|
from flask_json import as_json
|
|
from flask_login import current_user, login_required
|
|
|
|
from app import db, log
|
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
|
from app.api import api_permission_required as permission_required
|
|
from app.auth.models import User, Role, UserRole
|
|
from app.auth.models import is_valid_password
|
|
from app.decorators import scodoc
|
|
from app.models import Departement
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc.sco_permissions import Permission
|
|
from app.scodoc.sco_utils import json_error
|
|
|
|
|
|
@bp.route("/user/<int:uid>")
|
|
@api_web_bp.route("/user/<int:uid>")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersView)
|
|
@as_json
|
|
def user_info(uid: int):
|
|
"""
|
|
Info sur un compte utilisateur ScoDoc.
|
|
|
|
SAMPLES
|
|
-------
|
|
/user/2
|
|
"""
|
|
user: User = db.session.get(User, uid)
|
|
if user is None:
|
|
return json_error(404, "user not found")
|
|
if g.scodoc_dept:
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
|
|
if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
|
return json_error(404, "user not found")
|
|
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/users/query")
|
|
@api_web_bp.route("/users/query")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@as_json
|
|
def users_info_query():
|
|
"""Utilisateurs, filtrés par dept, active ou début nom
|
|
|
|
Exemple:
|
|
```
|
|
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
|
```
|
|
|
|
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
|
|
Si accès via API web, le département de l'URL est ignoré, seules
|
|
les permissions de l'utilisateur sont prises en compte.
|
|
|
|
QUERY
|
|
-----
|
|
active: bool
|
|
departement: string
|
|
starts_with: string
|
|
|
|
"""
|
|
query = User.query
|
|
active = request.args.get("active")
|
|
if active is not None:
|
|
active = bool(str(active))
|
|
query = query.filter_by(active=active)
|
|
departement = request.args.get("departement")
|
|
if departement is not None:
|
|
query = query.filter_by(dept=departement or None)
|
|
starts_with = request.args.get("starts_with")
|
|
if starts_with is not None:
|
|
# remove % and _ for security
|
|
starts_with = starts_with.translate({ord(c): None for c in "%_"})
|
|
query = query.filter(User.nom.ilike(starts_with + "%"))
|
|
# Filtre selon permissions:
|
|
query = (
|
|
query.join(UserRole, (UserRole.dept == User.dept) | (UserRole.dept == None))
|
|
.filter(UserRole.user == current_user)
|
|
.join(Role, UserRole.role_id == Role.id)
|
|
.filter(Role.permissions.op("&")(Permission.UsersView) != 0)
|
|
)
|
|
|
|
query = query.order_by(User.user_name)
|
|
return [user.to_dict() for user in query]
|
|
|
|
|
|
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
|
"Vrai si on peut"
|
|
if "cas_id" in args and not current_user.has_permission(
|
|
Permission.UsersChangeCASId
|
|
):
|
|
return False, "non autorise a changer cas_id"
|
|
|
|
if not current_user.is_administrator():
|
|
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
|
|
if field in args:
|
|
return False, f"non autorise a changer {field}"
|
|
return True, ""
|
|
|
|
|
|
@bp.route("/user/create", methods=["POST"])
|
|
@api_web_bp.route("/user/create", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersAdmin)
|
|
@as_json
|
|
def user_create():
|
|
"""Création d'un utilisateur
|
|
|
|
DATA
|
|
----
|
|
```json
|
|
{
|
|
"active":bool (default True),
|
|
"dept": str or null,
|
|
"nom": str,
|
|
"prenom": str,
|
|
"user_name": str,
|
|
...
|
|
}
|
|
```
|
|
"""
|
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
user_name = args.get("user_name")
|
|
if not user_name:
|
|
return json_error(404, "empty user_name")
|
|
user = User.query.filter_by(user_name=user_name).first()
|
|
if user:
|
|
return json_error(404, f"user_create: user {user} already exists\n")
|
|
dept = args.get("dept")
|
|
if dept == "@all":
|
|
dept = None
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
|
if (None not in allowed_depts) and (dept not in allowed_depts):
|
|
return json_error(403, "user_create: departement non autorise")
|
|
if (dept is not None) and (
|
|
Departement.query.filter_by(acronym=dept).first() is None
|
|
):
|
|
return json_error(404, "user_create: departement inexistant")
|
|
args["dept"] = dept
|
|
ok, msg = _is_allowed_user_edit(args)
|
|
if not ok:
|
|
return json_error(403, f"user_create: {msg}")
|
|
user = User(user_name=user_name)
|
|
user.from_dict(args, new_user=True)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/user/<int:uid>/edit", methods=["POST"])
|
|
@api_web_bp.route("/user/<int:uid>/edit", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersAdmin)
|
|
@as_json
|
|
def user_edit(uid: int):
|
|
"""Modification d'un utilisateur.
|
|
|
|
Champs modifiables:
|
|
```json
|
|
{
|
|
"dept": str or null,
|
|
"nom": str,
|
|
"prenom": str,
|
|
"active":bool
|
|
...
|
|
}
|
|
```
|
|
"""
|
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
user: User = User.get_or_404(uid)
|
|
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
|
orig_dept = user.dept
|
|
dest_dept = args.get("dept", False)
|
|
if dest_dept is not False:
|
|
if dest_dept == "@all":
|
|
dest_dept = None
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
|
if (None not in allowed_depts) and (
|
|
(orig_dept not in allowed_depts) or (dest_dept not in allowed_depts)
|
|
):
|
|
return json_error(403, "user_edit: departement non autorise")
|
|
if dest_dept != orig_dept:
|
|
if (dest_dept is not None) and (
|
|
Departement.query.filter_by(acronym=dest_dept).first() is None
|
|
):
|
|
return json_error(404, "user_edit: departement inexistant")
|
|
user.dept = dest_dept
|
|
|
|
ok, msg = _is_allowed_user_edit(args)
|
|
if not ok:
|
|
return json_error(403, f"user_edit: {msg}")
|
|
|
|
user.from_dict(args)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/user/<int:uid>/password", methods=["POST"])
|
|
@api_web_bp.route("/user/<int:uid>/password", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersAdmin)
|
|
@as_json
|
|
def user_password(uid: int):
|
|
"""Modification du mot de passe d'un utilisateur.
|
|
|
|
Si le mot de passe ne convient pas, erreur 400.
|
|
|
|
DATA
|
|
----
|
|
```json
|
|
{
|
|
"password": str
|
|
}
|
|
```
|
|
|
|
SAMPLES
|
|
-------
|
|
/user/3/password;{""password"" : ""rePlaCemeNT456averylongandcomplicated""}
|
|
"""
|
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
|
user: User = User.get_or_404(uid)
|
|
password = data.get("password")
|
|
if not password:
|
|
return json_error(404, "user_password: missing password")
|
|
if not is_valid_password(password):
|
|
return json_error(API_CLIENT_ERROR, "user_password: invalid password")
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
|
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
|
return json_error(403, "user_password: departement non autorise")
|
|
user.set_password(password)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/user/<int:uid>/role/<string:role_name>/add", methods=["POST"])
|
|
@api_web_bp.route("/user/<int:uid>/role/<string:role_name>/add", methods=["POST"])
|
|
@bp.route(
|
|
"/user/<int:uid>/role/<string:role_name>/add/departement/<string:dept>",
|
|
methods=["POST"],
|
|
)
|
|
@api_web_bp.route(
|
|
"/user/<int:uid>/role/<string:role_name>/add/departement/<string:dept>",
|
|
methods=["POST"],
|
|
)
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def user_role_add(uid: int, role_name: str, dept: str = None):
|
|
"""Ajoute un rôle à l'utilisateur dans le département donné."""
|
|
user: User = User.get_or_404(uid)
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
if dept is not None: # check
|
|
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin)
|
|
if (None not in allowed_depts) and (dept not in allowed_depts):
|
|
return json_error(403, "user_role_add: departement non autorise")
|
|
user.add_role(role, dept)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/user/<int:uid>/role/<string:role_name>/remove", methods=["POST"])
|
|
@api_web_bp.route("/user/<int:uid>/role/<string:role_name>/remove", methods=["POST"])
|
|
@bp.route(
|
|
"/user/<int:uid>/role/<string:role_name>/remove/departement/<string:dept>",
|
|
methods=["POST"],
|
|
)
|
|
@api_web_bp.route(
|
|
"/user/<int:uid>/role/<string:role_name>/remove/departement/<string:dept>",
|
|
methods=["POST"],
|
|
)
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
|
"""Retire le rôle (dans le département donné) à cet utilisateur."""
|
|
user: User = User.get_or_404(uid)
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
if dept is not None: # check
|
|
_ = Departement.query.filter_by(acronym=dept).first_or_404()
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin)
|
|
if (None not in allowed_depts) and (dept not in allowed_depts):
|
|
return json_error(403, "user_role_remove: departement non autorise")
|
|
|
|
query = UserRole.query.filter(UserRole.role == role, UserRole.user == user)
|
|
if dept is not None:
|
|
query = query.filter(UserRole.dept == dept)
|
|
user_role = query.first()
|
|
if user_role:
|
|
db.session.delete(user_role)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.to_dict()
|
|
|
|
|
|
@bp.route("/permissions")
|
|
@api_web_bp.route("/permissions")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersView)
|
|
@as_json
|
|
def permissions_list():
|
|
"""Liste des noms de permissions définies.
|
|
|
|
SAMPLES
|
|
-------
|
|
/permissions
|
|
"""
|
|
return list(Permission.permission_by_name.keys())
|
|
|
|
|
|
@bp.route("/role/<string:role_name>")
|
|
@api_web_bp.route("/role/<string:role_name>")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersView)
|
|
@as_json
|
|
def role_get(role_name: str):
|
|
"""Un rôle.
|
|
|
|
SAMPLES
|
|
-------
|
|
/role/Ens
|
|
"""
|
|
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
|
|
|
|
|
@bp.route("/roles")
|
|
@api_web_bp.route("/roles")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.UsersView)
|
|
@as_json
|
|
def roles_list():
|
|
"""Tous les rôles définis.
|
|
|
|
SAMPLES
|
|
-------
|
|
/roles
|
|
"""
|
|
return [role.to_dict() for role in Role.query]
|
|
|
|
|
|
@bp.route(
|
|
"/role/<string:role_name>/add_permission/<string:perm_name>",
|
|
methods=["POST"],
|
|
)
|
|
@api_web_bp.route(
|
|
"/role/<string:role_name>/add_permission/<string:perm_name>",
|
|
methods=["POST"],
|
|
)
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def role_permission_add(role_name: str, perm_name: str):
|
|
"""Ajoute une permission à un rôle."""
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
permission = Permission.get_by_name(perm_name)
|
|
if permission is None:
|
|
return json_error(404, "role_permission_add: permission inconnue")
|
|
role.add_permission(permission)
|
|
db.session.add(role)
|
|
db.session.commit()
|
|
log(f"role_permission_add({role_name}, {perm_name})")
|
|
return role.to_dict()
|
|
|
|
|
|
@bp.route(
|
|
"/role/<string:role_name>/remove_permission/<string:perm_name>",
|
|
methods=["POST"],
|
|
)
|
|
@api_web_bp.route(
|
|
"/role/<string:role_name>/remove_permission/<string:perm_name>",
|
|
methods=["POST"],
|
|
)
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def role_permission_remove(role_name: str, perm_name: str):
|
|
"""Retire une permission d'un rôle."""
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
permission = Permission.get_by_name(perm_name)
|
|
if permission is None:
|
|
return json_error(404, "role_permission_remove: permission inconnue")
|
|
role.remove_permission(permission)
|
|
db.session.add(role)
|
|
db.session.commit()
|
|
log(f"role_permission_remove({role_name}, {perm_name})")
|
|
return role.to_dict()
|
|
|
|
|
|
@bp.route("/role/create/<string:role_name>", methods=["POST"])
|
|
@api_web_bp.route("/role/create/<string:role_name>", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def role_create(role_name: str):
|
|
"""Création d'un nouveau rôle avec les permissions données.
|
|
|
|
DATA
|
|
----
|
|
```json
|
|
{
|
|
"permissions" : [ 'ScoView', ... ]
|
|
}
|
|
```
|
|
|
|
SAMPLES
|
|
-------
|
|
/role/create/customRole;{""permissions"": [""ScoView"", ""UsersView""]}
|
|
"""
|
|
role: Role = Role.query.filter_by(name=role_name).first()
|
|
if role:
|
|
return json_error(404, "role_create: role already exists")
|
|
role = Role(name=role_name)
|
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
|
permissions = data.get("permissions")
|
|
if permissions:
|
|
try:
|
|
role.set_named_permissions(permissions)
|
|
except ScoValueError:
|
|
return json_error(404, "role_create: invalid permissions")
|
|
db.session.add(role)
|
|
db.session.commit()
|
|
return role.to_dict()
|
|
|
|
|
|
@bp.route("/role/<string:role_name>/edit", methods=["POST"])
|
|
@api_web_bp.route("/role/<string:role_name>/edit", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def role_edit(role_name: str):
|
|
"""Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
|
|
|
|
DATA
|
|
----
|
|
```json
|
|
{
|
|
"name" : name
|
|
"permissions" : [ 'ScoView', ... ]
|
|
}
|
|
```
|
|
"""
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
|
permissions = data.get("permissions", False)
|
|
if permissions is not False:
|
|
try:
|
|
role.set_named_permissions(permissions)
|
|
except ScoValueError:
|
|
return json_error(404, "role_create: invalid permissions")
|
|
role_name = data.get("role_name")
|
|
if role_name and role_name != role.name:
|
|
existing_role: Role = Role.query.filter_by(name=role_name).first()
|
|
if existing_role:
|
|
return json_error(404, "role_edit: role name already exists")
|
|
role.name = role_name
|
|
db.session.add(role)
|
|
db.session.commit()
|
|
return role.to_dict()
|
|
|
|
|
|
@bp.route("/role/<string:role_name>/delete", methods=["POST"])
|
|
@api_web_bp.route("/role/<string:role_name>/delete", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoSuperAdmin)
|
|
@as_json
|
|
def role_delete(role_name: str):
|
|
"""Suppression d'un rôle.
|
|
|
|
SAMPLES
|
|
-------
|
|
/role/customRole/delete
|
|
"""
|
|
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
|
db.session.delete(role)
|
|
db.session.commit()
|
|
return {"OK": True}
|
|
|
|
|
|
# @bp.route("/user/<int:uid>/edt")
|
|
# @api_web_bp.route("/user/<int:uid>/edt")
|
|
# @login_required
|
|
# @scodoc
|
|
# @permission_required(Permission.ScoView)
|
|
# @as_json
|
|
# def user_edt(uid: int):
|
|
# """L'emploi du temps de l'utilisateur.
|
|
# Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
|
|
|
# show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
|
|
|
# Il faut la permission ScoView + (UsersView ou bien être connecté comme l'utilisateur demandé)
|
|
# """
|
|
# if g.scodoc_dept is None: # route API non départementale
|
|
# if not current_user.has_permission(Permission.UsersView):
|
|
# return scu.json_error(403, "accès non autorisé")
|
|
# user: User = db.session.get(User, uid)
|
|
# if user is None:
|
|
# return json_error(404, "user not found")
|
|
# # Check permission
|
|
# if current_user.id != user.id:
|
|
# if g.scodoc_dept:
|
|
# allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
|
|
# if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
|
# return json_error(404, "user not found")
|
|
|
|
# show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
|
|
|
# # Cherche ics
|
|
# if not user.edt_id:
|
|
# return json_error(404, "user not configured")
|
|
# ics_filename = sco_edt_cal.get_ics_user_edt_filename(user.edt_id)
|
|
# if not ics_filename:
|
|
# return json_error(404, "no calendar for this user")
|
|
|
|
# _, calendar = sco_edt_cal.load_calendar(ics_filename)
|
|
|
|
# # TODO:
|
|
# # - Construire mapping edt2modimpl: edt_id -> modimpl
|
|
# # pour cela, considérer tous les formsemestres de la période de l'edt
|
|
# # (soit on considère l'année scolaire du 1er event, ou celle courante,
|
|
# # soit on cherche min, max des dates des events)
|
|
# # - Modifier décodage des groupes dans convert_ics pour avoi run mapping
|
|
# # de groupe par semestre (retrouvé grâce au modimpl associé à l'event)
|
|
|
|
# raise NotImplementedError() # TODO XXX WIP
|
|
|
|
# events_scodoc, _ = sco_edt_cal.convert_ics(
|
|
# calendar,
|
|
# edt2group=edt2group,
|
|
# default_group=default_group,
|
|
# edt2modimpl=edt2modimpl,
|
|
# )
|
|
# edt_dict = sco_edt_cal.translate_calendar(
|
|
# events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
|
# )
|
|
# return edt_dict
|