From a053afeba646d4cc90eb41c45387aa67c065bd5c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 7 Aug 2022 11:08:12 +0200 Subject: [PATCH] API: gestion utilisateurs --- app/api/users.py | 143 ++++++++++++++++++++++++++++++++-- app/auth/models.py | 25 ++++-- app/scodoc/sco_permissions.py | 2 + tests/api/test_api_users.py | 28 ++++++- 4 files changed, 182 insertions(+), 16 deletions(-) diff --git a/app/api/users.py b/app/api/users.py index 232f79f037..198744222d 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -51,7 +51,7 @@ def user_info(uid: int): @permission_required(Permission.ScoView) def users_info_query(): """Utilisateurs, filtrés par dept, active ou début nom - /users/query?departement=dept_acronym&active=1&starts_with= + /users/query?departement=dept_acronym&active=1&starts_with= Si accès via API web, seuls les utilisateurs "accessibles" (selon les permissions) sont retournés: le département de l'URL est ignoré, seules @@ -230,21 +230,148 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): return jsonify(user.to_dict()) +@bp.route("/permissions") +@api_web_bp.route("/permissions") +@login_required +@scodoc +@permission_required(Permission.ScoUsersView) +def list_permissions(): + """Liste des noms de permissions définies""" + return jsonify(list(Permission.permission_by_name.keys())) + + +@bp.route("/role/") +@api_web_bp.route("/role/") +@login_required +@scodoc +@permission_required(Permission.ScoUsersView) +def list_role(role_name: str): + """Un rôle""" + return jsonify(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.ScoView) +@permission_required(Permission.ScoUsersView) 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") +@bp.route( + "/role//add_permission/", + methods=["POST"], +) +@api_web_bp.route( + "/role//add_permission/", + methods=["POST"], +) @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())) +@permission_required(Permission.ScoSuperAdmin) +def role_permission_add(role_name: str, perm_name: str): + """Add permission to role""" + role: Role = Role.query.filter_by(name=role_name).first_or_404() + permission = Permission.get_by_name(perm_name) + if permission is None: + return error_response(404, "role_permission_add: permission inconnue") + role.add_permission(permission) + db.session.add(role) + db.session.commit() + return jsonify(role.to_dict()) + + +@bp.route( + "/role//remove_permission/", + methods=["POST"], +) +@api_web_bp.route( + "/role//remove_permission/", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def role_permission_remove(role_name: str, perm_name: str): + """Remove permission from role""" + role: Role = Role.query.filter_by(name=role_name).first_or_404() + permission = Permission.get_by_name(perm_name) + if permission is None: + return error_response(404, "role_permission_remove: permission inconnue") + role.remove_permission(permission) + db.session.add(role) + db.session.commit() + return jsonify(role.to_dict()) + + +@bp.route("/role//create", methods=["POST"]) +@api_web_bp.route("/role//create", methods=["POST"]) +@login_required +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def role_create(role_name: str): + """Create a new role with permissions. + { + "permissions" : [ 'ScoView', ... ] + } + """ + role: Role = Role.query.filter_by(name=role_name).first() + if role: + return error_response(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 error_response(404, "role_create: invalid permissions") + db.session.add(role) + db.session.commit() + return jsonify(role.to_dict()) + + +@bp.route("/role//edit", methods=["POST"]) +@api_web_bp.route("/role//edit", methods=["POST"]) +@login_required +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def role_edit(role_name: str): + """Edit a role. On peut spécifier un nom et/ou des permissions. + { + "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 error_response(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 error_response(404, "role_edit: role name already exists") + role.name = role_name + db.session.add(role) + db.session.commit() + return jsonify(role.to_dict()) + + +@bp.route("/role//delete", methods=["POST"]) +@api_web_bp.route("/role//delete", methods=["POST"]) +@login_required +@scodoc +@permission_required(Permission.ScoSuperAdmin) +def role_delete(role_name: str): + """Delete a role""" + role: Role = Role.query.filter_by(name=role_name).first_or_404() + db.session.delete(role) + db.session.commit() + return jsonify({"OK": True}) diff --git a/app/auth/models.py b/app/auth/models.py index c8240343b5..274be8e7a2 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -407,11 +407,10 @@ class Role(db.Model): """Roles for ScoDoc""" id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), unique=True) # TODO: , nullable=False)) + name = db.Column(db.String(64), unique=True, nullable=False, index=True) default = db.Column(db.Boolean, default=False, index=True) permissions = db.Column(db.BigInteger) # 64 bits users = db.relationship("User", secondary="user_role", viewonly=True) - # __table_args__ = (db.UniqueConstraint("name", "dept", name="_rolename_dept_uc"),) def __init__(self, **kwargs): super(Role, self).__init__(**kwargs) @@ -432,20 +431,34 @@ class Role(db.Model): "As dict. Convert permissions to names." return { "id": self.id, - "name": self.name, + "role_name": self.name, # pour être cohérent avec partion_name, etc. "permissions": Permission.permissions_names(self.permissions), } - def add_permission(self, perm): + def add_permission(self, perm: int): + "Add permission to role" self.permissions |= perm - def remove_permission(self, perm): + def remove_permission(self, perm: int): + "Remove permission from role" self.permissions = self.permissions & ~perm def reset_permissions(self): + "Remove all permissions from role" self.permissions = 0 - def has_permission(self, perm): + def set_named_permissions(self, permission_names: list[str]): + """Set permissions, given as a list of permissions names. + Raises ScoValueError if invalid permission.""" + self.permissions = 0 + for permission_name in permission_names: + permission = Permission.get_by_name(permission_name) + if permission is None: + raise ScoValueError("set_named_permissions: invalid permission name") + self.permissions |= permission + + def has_permission(self, perm: int) -> bool: + "True if role as this permission" return self.permissions & perm == perm @staticmethod diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index ba9f474e6e..cd54f7e3b0 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -94,6 +94,8 @@ class Permission: def permissions_names(permissions: int) -> list[str]: """From a bit field, return list of permission names""" names = [] + if permissions == 0: + return [] mask = 1 << (permissions.bit_length() - 1) while mask > 0: if mask & permissions: diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index 76fc75ad7e..5ddc384689 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -80,14 +80,14 @@ def test_edit_users(api_admin_headers): 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)) + 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["dept"] == "TAPI" assert user["active"] is False @@ -105,3 +105,27 @@ def test_roles(api_admin_headers): uid = user["id"] ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h) assert ans["user_name"] == "test_roles" + role = POST_JSON("/role/Test_X/create", headers=admin_h) + assert role["role_name"] == "Test_X" + assert role["permissions"] == [] + role = GET("/role/Test_X", headers=admin_h) + assert role["role_name"] == "Test_X" + assert role["permissions"] == [] + role = POST_JSON("/role/Test_X/edit", {"role_name": "Test_Y"}, headers=admin_h) + assert role["role_name"] == "Test_Y" + role = GET("/role/Test_Y", headers=admin_h) + assert role["role_name"] == "Test_Y" + role = POST_JSON( + "/role/Test_Y/edit", + {"permissions": ["ScoView", "ScoAbsChange"]}, + headers=admin_h, + ) + assert set(role["permissions"]) == {"ScoView", "ScoAbsChange"} + role = POST_JSON("/role/Test_Y/add_permission/ScoAbsAddBillet", headers=admin_h) + assert set(role["permissions"]) == {"ScoView", "ScoAbsChange", "ScoAbsAddBillet"} + role = GET("/role/Test_Y", headers=admin_h) + assert set(role["permissions"]) == {"ScoView", "ScoAbsChange", "ScoAbsAddBillet"} + role = POST_JSON("/role/Test_Y/remove_permission/ScoAbsChange", headers=admin_h) + assert set(role["permissions"]) == {"ScoView", "ScoAbsAddBillet"} + ans = POST_JSON("/role/Test_Y/delete", headers=admin_h) + assert ans["OK"] is True