API: users, WIP

This commit is contained in:
Emmanuel Viennet 2022-08-06 22:31:41 +02:00
parent dae762c3b1
commit a069280746
9 changed files with 243 additions and 24 deletions

View File

@ -21,6 +21,7 @@ from app.decorators import scodoc, permission_required
from app.models import Departement from app.models import Departement
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
@bp.route("/user/<int:uid>") @bp.route("/user/<int:uid>")
@ -78,4 +79,172 @@ def users_info_query():
) )
query = query.order_by(User.user_name) query = query.order_by(User.user_name)
return jsonify([u.to_dict() for u in query]) return jsonify([user.to_dict() for user in query])
@bp.route("/user/create", methods=["POST"])
@api_web_bp.route("/user/create", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoUsersAdmin)
def user_create():
"""Création d'un utilisateur
The request content type should be "application/json":
{
"user_name": str,
"dept": str or null,
"nom": str,
"prenom": str,
"active":bool (default True)
}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
user_name = data.get("user_name")
if not user_name:
return error_response(404, "empty user_name")
user = User.query.filter_by(user_name=user_name).first()
if user:
return error_response(404, f"user_create: user {user} already exists\n")
dept = data.get("dept")
if dept == "@all":
dept = None
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
if dept not in allowed_depts:
return error_response(403, "user_create: departement non autorise")
if (dept is not None) and (
Departement.query.filter_by(acronym=dept).first() is None
):
return error_response(404, "user_create: departement inexistant")
nom = data.get("nom")
prenom = data.get("prenom")
active = scu.to_bool(data.get("active", True))
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict())
@bp.route("/user/edit/<int:uid>", methods=["POST"])
@api_web_bp.route("/user/edit/<int:uid>", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoUsersAdmin)
def user_edit(uid: int):
"""Modification d'un utilisateur
Champs modifiables:
{
"dept": str or null,
"nom": str,
"prenom": str,
"active":bool
}
"""
data = request.get_json(force=True) # may raise 400 Bad Request
user: User = User.query.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 = data.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.ScoUsersAdmin)
if (None not in allowed_depts) and (
(orig_dept not in allowed_depts) or (dest_dept not in allowed_depts)
):
return error_response(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 error_response(404, "user_edit: departement inexistant")
user.dept = dest_dept
user.nom = data.get("nom", user.nom)
user.prenom = data.get("prenom", user.prenom)
user.active = scu.to_bool(data.get("active", user.active))
db.session.add(user)
db.session.commit()
return jsonify(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)
def user_role_add(uid: int, role_name: str, dept: str = None):
"""Add a role to the user"""
user: User = User.query.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 error_response(403, "user_role_add: departement non autorise")
user.add_role(role, dept)
db.session.add(user)
db.session.commit()
return jsonify(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)
def user_role_remove(uid: int, role_name: str, dept: str = None):
"""Remove the role from the user"""
user: User = User.query.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 error_response(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 jsonify(user.to_dict())
@bp.route("/roles")
@api_web_bp.route("/roles")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def list_roles():
"""Tous les rôles définis"""
return jsonify([role.to_dict() for role in Role.query])
@bp.route("/permissions")
@api_web_bp.route("/permissions")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def list_permissions():
"""Liste des noms de permissions définies"""
return jsonify(list(Permission.permission_by_name.keys()))

View File

@ -428,6 +428,14 @@ class Role(db.Model):
def __str__(self): def __str__(self):
return f"{self.name}: perm={', '.join(Permission.permissions_names(self.permissions))}" return f"{self.name}: perm={', '.join(Permission.permissions_names(self.permissions))}"
def to_dict(self) -> dict:
"As dict. Convert permissions to names."
return {
"id": self.id,
"name": self.name,
"permissions": Permission.permissions_names(self.permissions),
}
def add_permission(self, perm): def add_permission(self, perm):
self.permissions |= perm self.permissions |= perm

View File

@ -600,22 +600,6 @@ def float_null_is_null(x):
return float(x) return float(x)
BOOL_STR = {
"": False,
"false": False,
"0": False,
"1": True,
"true": True,
}
def bool_or_str(x) -> bool:
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
if isinstance(x, str):
return BOOL_STR[x.lower()]
return bool(x)
# post filtering # post filtering
# #
def UniqListofDicts(L, key): def UniqListofDicts(L, key):

View File

@ -42,7 +42,6 @@ from app.scodoc.scolog import logdb
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -807,7 +806,7 @@ def ListeAbsEtud(
sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005" sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005"
""" """
# si absjust_only, table absjust seule (export xls ou pdf) # si absjust_only, table absjust seule (export xls ou pdf)
absjust_only = ndb.bool_or_str(absjust_only) absjust_only = scu.to_bool(absjust_only)
datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year) datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year)
etudid = etudid or False etudid = etudid or False
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)

View File

@ -91,7 +91,7 @@ _ueEditor = ndb.EditableTable(
sortkey="numero", sortkey="numero",
input_formators={ input_formators={
"type": ndb.int_null_is_zero, "type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str, "is_external": scu.to_bool,
"ects": ndb.float_null_is_null, "ects": ndb.float_null_is_null,
}, },
output_formators={ output_formators={

View File

@ -9,7 +9,7 @@
_SCO_PERMISSIONS = ( _SCO_PERMISSIONS = (
# permission bit, symbol, description # permission bit, symbol, description
# ScoSuperAdmin est utilisé pour: # ScoSuperAdmin est utilisé pour:
# - ZScoDoc: add/delete departments # - add/delete departments
# - tous rôles lors creation utilisateurs # - tous rôles lors creation utilisateurs
(1 << 1, "ScoSuperAdmin", "Super Administrateur"), (1 << 1, "ScoSuperAdmin", "Super Administrateur"),
(1 << 2, "APIView", "Voir (obsolete, use ScoView)"), # deprecated (1 << 2, "APIView", "Voir (obsolete, use ScoView)"), # deprecated

View File

@ -638,6 +638,22 @@ def is_valid_filename(filename):
return VALID_EXP.match(filename) return VALID_EXP.match(filename)
BOOL_STR = {
"": False,
"false": False,
"0": False,
"1": True,
"true": True,
}
def to_bool(x) -> bool:
"""a boolean, may also be encoded as a string "0", "False", "1", "True" """
if isinstance(x, str):
return BOOL_STR.get(x.lower().strip(), True)
return bool(x)
def bul_filename_old(sem: dict, etud: dict, format): def bul_filename_old(sem: dict, etud: dict, format):
"""Build a filename for this bulletin""" """Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d") dt = time.strftime("%Y-%m-%d")

View File

@ -337,7 +337,8 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
user_role = UserRole.query.filter( user_role = UserRole.query.filter(
UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym
).first() ).first()
db.session.delete(user_role) if user_role:
db.session.delete(user_role)
db.session.commit() db.session.commit()
@ -352,8 +353,8 @@ def abort_if_false(ctx, param, value):
is_flag=True, is_flag=True,
callback=abort_if_false, callback=abort_if_false,
expose_value=False, expose_value=False,
prompt=f"""Attention: Cela va effacer toutes les données du département prompt="""Attention: Cela va effacer toutes les données du département
(étudiants, notes, formations, etc) (étudiants, notes, formations, etc).
Voulez-vous vraiment continuer ? Voulez-vous vraiment continuer ?
""", """,
) )

View File

@ -63,3 +63,45 @@ def test_list_users(api_admin_headers):
# (test, other et u_TAPI) # (test, other et u_TAPI)
# plus l'utilisateur de chaque département jusqu'au leur # plus l'utilisateur de chaque département jusqu'au leur
# (u_AA voit AA, u_BB voit AA et BB, etc) # (u_AA voit AA, u_BB voit AA et BB, etc)
def test_edit_users(api_admin_headers):
"""
Routes: /user/create
/user/edit/<int:uid>
"""
admin_h = api_admin_headers
nb_users = len(GET("/users/query", headers=admin_h))
user = POST_JSON(
"/user/create",
{"user_name": "toto", "nom": "Toto"},
headers=admin_h,
)
assert user["user_name"] == "toto"
assert user["dept"] is None
assert user["active"] is True
assert nb_users == 1 + len(GET("/users/query", headers=admin_h))
# Change le dept et rend inactif
user = POST_JSON(
f"/user/edit/{user['id']}",
{"active": False, "dept": "TAPI"},
headers=admin_h,
)
assert user["dept"] is "TAPI"
assert user["active"] is False
def test_roles(api_admin_headers):
"""
Routes: /user/create
/user/edit/<int:uid>
"""
admin_h = api_admin_headers
user = POST_JSON(
"/user/create",
{"user_name": "test_roles", "nom": "Role", "prenom": "Test"},
headers=admin_h,
)
uid = user["id"]
ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h)
assert ans["user_name"] == "test_roles"