about summary refs log tree commit diff
path: root/gn_auth
diff options
context:
space:
mode:
authorClaude Sonnet 4.62026-08-27 16:22:02 +0000
committerFrederick Muriuki Muriithi2026-08-27 11:23:58 -0500
commit6e33b2b43a76d3650c5b04f3f30b2e9fc859891a (patch)
tree8688b2da6a19e8565e22b438abc3c2264a23d9dd /gn_auth
parent4a8ffa773ad487d1e0ad96ded074f4efcf8a438c (diff)
downloadgn-auth-6e33b2b43a76d3650c5b04f3f30b2e9fc859891a.tar.gz
feat(users/admin): implement POST /auth/user/<uid>/roles/revoke
Adds unassign_user_role_by_name to roles/models.py — mirrors
assign_user_role_by_name but issues a resource-scoped DELETE,
resolving the TODO on the older revoke_user_role_by_name which
lacked the resource_id filter.

The new revoke_user_role endpoint checks resource:user:assign-role
(same privilege as assign) via can_assign_role, then calls
unassign_user_role_by_name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewed-By: Frederick M. Muriithi <fredmanglis@gmail.com>
Diffstat (limited to 'gn_auth')
-rw-r--r--gn_auth/auth/authorisation/roles/models.py19
-rw-r--r--gn_auth/auth/authorisation/users/views.py27
2 files changed, 45 insertions, 1 deletions
diff --git a/gn_auth/auth/authorisation/roles/models.py b/gn_auth/auth/authorisation/roles/models.py
index 6faeaca..89403f8 100644
--- a/gn_auth/auth/authorisation/roles/models.py
+++ b/gn_auth/auth/authorisation/roles/models.py
@@ -255,6 +255,25 @@ def assign_user_role_by_name(
             })
 
 
+def unassign_user_role_by_name(
+        cursor: db.DbCursor, user: User, resource_id: UUID, role_name: str):
+    """Revoke a role from `user` on `resource_id` by the role's name."""
+    cursor.execute(
+        "SELECT role_id FROM roles WHERE role_name=:role_name",
+        {"role_name": role_name})
+    role = cursor.fetchone()
+    if role:
+        cursor.execute(
+            ("DELETE FROM user_roles "
+             "WHERE user_id=:user_id AND role_id=:role_id "
+             "AND resource_id=:resource_id"),
+            {
+                "user_id": str(user.user_id),
+                "role_id": role["role_id"],
+                "resource_id": str(resource_id)
+            })
+
+
 def role_by_id(conn: db.DbConnection, role_id: UUID) -> Optional[Role]:
     """Fetch a role from the database by its ID."""
     with db.cursor(conn) as cursor:
diff --git a/gn_auth/auth/authorisation/users/views.py b/gn_auth/auth/authorisation/users/views.py
index ae33630..5cdb482 100644
--- a/gn_auth/auth/authorisation/users/views.py
+++ b/gn_auth/auth/authorisation/users/views.py
@@ -41,7 +41,7 @@ from gn_auth.auth.authorisation.resources.checks import authorised_for2
 from gn_auth.auth.authorisation.resources.models import (
     user_resources as _user_resources)
 from gn_auth.auth.authorisation.roles.models import (
-    assign_default_roles, assign_user_role_by_name,
+    assign_default_roles, assign_user_role_by_name, unassign_user_role_by_name,
     user_roles as _user_roles, user_roles_on_resource)
 from gn_auth.auth.authorisation.resources.groups.models import (
     user_group as _user_group)
@@ -827,3 +827,28 @@ def assign_user_role(user_id: uuid.UUID) -> Response:
         "role_name": form["role_name"],
         "resource_id": form["resource_id"]
     }), 200)
+
+
+@users.route("/<uuid:user_id>/roles/revoke", methods=["POST"])
+def revoke_user_role(user_id: uuid.UUID) -> Response:
+    """Revoke a role from a user on a given resource."""
+    with (require_oauth.acquire("profile user resource role") as token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        form = request_json()
+        resource_id = uuid.UUID(form["resource_id"])
+        caller_roles = user_roles_on_resource(
+            conn, token.user.user_id, resource_id)
+        if not can_assign_role(tuple(
+                priv.privilege_id for role in caller_roles
+                for priv in role.privileges)):
+            raise ForbiddenAccess(
+                "You need the `resource:user:assign-role` privilege.")
+        target = user_by_id(conn, user_id)
+        with db.cursor(conn) as cursor:
+            unassign_user_role_by_name(
+                cursor, target, resource_id, form["role_name"])
+    return make_response(jsonify({
+        "user_id": str(user_id),
+        "role_name": form["role_name"],
+        "resource_id": form["resource_id"]
+    }), 200)