From a0692807462a156eff61eefdbcc5445c65a923ee Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 6 Aug 2022 22:31:41 +0200 Subject: [PATCH] API: users, WIP --- app/api/users.py | 171 +++++++++++++++++++++++++++++++++- app/auth/models.py | 8 ++ app/scodoc/notesdb.py | 16 ---- app/scodoc/sco_abs_views.py | 3 +- app/scodoc/sco_edit_ue.py | 2 +- app/scodoc/sco_permissions.py | 2 +- app/scodoc/sco_utils.py | 16 ++++ scodoc.py | 7 +- tests/api/test_api_users.py | 42 +++++++++ 9 files changed, 243 insertions(+), 24 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 55d160886..232f79f03 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -21,6 +21,7 @@ from app.decorators import scodoc, permission_required from app.models import Departement from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission +from app.scodoc import sco_utils as scu @bp.route("/user/") @@ -78,4 +79,172 @@ def users_info_query(): ) 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/", methods=["POST"]) +@api_web_bp.route("/user/edit/", 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//role//add", methods=["POST"]) +@api_web_bp.route("/user//role//add", methods=["POST"]) +@bp.route( + "/user//role//add/departement/", + methods=["POST"], +) +@api_web_bp.route( + "/user//role//add/departement/", + 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//role//remove", methods=["POST"]) +@api_web_bp.route("/user//role//remove", methods=["POST"]) +@bp.route( + "/user//role//remove/departement/", + methods=["POST"], +) +@api_web_bp.route( + "/user//role//remove/departement/", + 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())) diff --git a/app/auth/models.py b/app/auth/models.py index d8342cc37..c8240343b 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -428,6 +428,14 @@ class Role(db.Model): def __str__(self): 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): self.permissions |= perm diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 7ad7896ae..196274955 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -600,22 +600,6 @@ def float_null_is_null(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 # def UniqListofDicts(L, key): diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index c01588df6..ec8eaf1f4 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -42,7 +42,6 @@ from app.scodoc.scolog import logdb from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_abs -from app.scodoc import sco_cache from app.scodoc import sco_etud from app.scodoc import sco_find_etud 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" """ # 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) etudid = etudid or False etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 6658a6350..4cc745027 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -91,7 +91,7 @@ _ueEditor = ndb.EditableTable( sortkey="numero", input_formators={ "type": ndb.int_null_is_zero, - "is_external": ndb.bool_or_str, + "is_external": scu.to_bool, "ects": ndb.float_null_is_null, }, output_formators={ diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 7e1316930..ba9f474e6 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -9,7 +9,7 @@ _SCO_PERMISSIONS = ( # permission bit, symbol, description # ScoSuperAdmin est utilisé pour: - # - ZScoDoc: add/delete departments + # - add/delete departments # - tous rôles lors creation utilisateurs (1 << 1, "ScoSuperAdmin", "Super Administrateur"), (1 << 2, "APIView", "Voir (obsolete, use ScoView)"), # deprecated diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 6f133fe17..2fa6617f9 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -638,6 +638,22 @@ def is_valid_filename(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): """Build a filename for this bulletin""" dt = time.strftime("%Y-%m-%d") diff --git a/scodoc.py b/scodoc.py index ef464a099..e72cc754d 100755 --- a/scodoc.py +++ b/scodoc.py @@ -337,7 +337,8 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name= user_role = UserRole.query.filter( UserRole.role == role, UserRole.user == user, UserRole.dept == dept_acronym ).first() - db.session.delete(user_role) + if user_role: + db.session.delete(user_role) db.session.commit() @@ -352,8 +353,8 @@ def abort_if_false(ctx, param, value): is_flag=True, callback=abort_if_false, expose_value=False, - prompt=f"""Attention: Cela va effacer toutes les données du département - (étudiants, notes, formations, etc) + prompt="""Attention: Cela va effacer toutes les données du département + (étudiants, notes, formations, etc). Voulez-vous vraiment continuer ? """, ) diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index ddc25b498..76fc75ad7 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -63,3 +63,45 @@ def test_list_users(api_admin_headers): # (test, other et u_TAPI) # plus l'utilisateur de chaque département jusqu'au leur # (u_AA voit AA, u_BB voit AA et BB, etc) + + +def test_edit_users(api_admin_headers): + """ + Routes: /user/create + /user/edit/ + """ + 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/ + """ + 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"