API: gestion utilisateurs

This commit is contained in:
Emmanuel Viennet 2022-08-07 11:08:12 +02:00
parent a069280746
commit a053afeba6
4 changed files with 182 additions and 16 deletions

View File

@ -51,7 +51,7 @@ def user_info(uid: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def users_info_query(): def users_info_query():
"""Utilisateurs, filtrés par dept, active ou début nom """Utilisateurs, filtrés par dept, active ou début nom
/users/query?departement=dept_acronym&active=1&starts_with=<str:nom> /users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
Si accès via API web, seuls les utilisateurs "accessibles" (selon les 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 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()) 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/<string:role_name>")
@api_web_bp.route("/role/<string:role_name>")
@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") @bp.route("/roles")
@api_web_bp.route("/roles") @api_web_bp.route("/roles")
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoUsersView)
def list_roles(): def list_roles():
"""Tous les rôles définis""" """Tous les rôles définis"""
return jsonify([role.to_dict() for role in Role.query]) return jsonify([role.to_dict() for role in Role.query])
@bp.route("/permissions") @bp.route(
@api_web_bp.route("/permissions") "/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 @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoSuperAdmin)
def list_permissions(): def role_permission_add(role_name: str, perm_name: str):
"""Liste des noms de permissions définies""" """Add permission to role"""
return jsonify(list(Permission.permission_by_name.keys())) 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/<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)
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/<string:role_name>/create", methods=["POST"])
@api_web_bp.route("/role/<string:role_name>/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/<string:role_name>/edit", methods=["POST"])
@api_web_bp.route("/role/<string:role_name>/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/<string:role_name>/delete", methods=["POST"])
@api_web_bp.route("/role/<string:role_name>/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})

View File

@ -407,11 +407,10 @@ class Role(db.Model):
"""Roles for ScoDoc""" """Roles for ScoDoc"""
id = db.Column(db.Integer, primary_key=True) 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) default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.BigInteger) # 64 bits permissions = db.Column(db.BigInteger) # 64 bits
users = db.relationship("User", secondary="user_role", viewonly=True) users = db.relationship("User", secondary="user_role", viewonly=True)
# __table_args__ = (db.UniqueConstraint("name", "dept", name="_rolename_dept_uc"),)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Role, self).__init__(**kwargs) super(Role, self).__init__(**kwargs)
@ -432,20 +431,34 @@ class Role(db.Model):
"As dict. Convert permissions to names." "As dict. Convert permissions to names."
return { return {
"id": self.id, "id": self.id,
"name": self.name, "role_name": self.name, # pour être cohérent avec partion_name, etc.
"permissions": Permission.permissions_names(self.permissions), "permissions": Permission.permissions_names(self.permissions),
} }
def add_permission(self, perm): def add_permission(self, perm: int):
"Add permission to role"
self.permissions |= perm self.permissions |= perm
def remove_permission(self, perm): def remove_permission(self, perm: int):
"Remove permission from role"
self.permissions = self.permissions & ~perm self.permissions = self.permissions & ~perm
def reset_permissions(self): def reset_permissions(self):
"Remove all permissions from role"
self.permissions = 0 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 return self.permissions & perm == perm
@staticmethod @staticmethod

View File

@ -94,6 +94,8 @@ class Permission:
def permissions_names(permissions: int) -> list[str]: def permissions_names(permissions: int) -> list[str]:
"""From a bit field, return list of permission names""" """From a bit field, return list of permission names"""
names = [] names = []
if permissions == 0:
return []
mask = 1 << (permissions.bit_length() - 1) mask = 1 << (permissions.bit_length() - 1)
while mask > 0: while mask > 0:
if mask & permissions: if mask & permissions:

View File

@ -80,14 +80,14 @@ def test_edit_users(api_admin_headers):
assert user["user_name"] == "toto" assert user["user_name"] == "toto"
assert user["dept"] is None assert user["dept"] is None
assert user["active"] is True 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 # Change le dept et rend inactif
user = POST_JSON( user = POST_JSON(
f"/user/edit/{user['id']}", f"/user/edit/{user['id']}",
{"active": False, "dept": "TAPI"}, {"active": False, "dept": "TAPI"},
headers=admin_h, headers=admin_h,
) )
assert user["dept"] is "TAPI" assert user["dept"] == "TAPI"
assert user["active"] is False assert user["active"] is False
@ -105,3 +105,27 @@ def test_roles(api_admin_headers):
uid = user["id"] uid = user["id"]
ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h) ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h)
assert ans["user_name"] == "test_roles" 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