diff options
| author | Claude Sonnet 4.6 | 2026-08-27 16:22:02 +0000 |
|---|---|---|
| committer | Frederick Muriuki Muriithi | 2026-08-27 11:23:58 -0500 |
| commit | 6e33b2b43a76d3650c5b04f3f30b2e9fc859891a (patch) | |
| tree | 8688b2da6a19e8565e22b438abc3c2264a23d9dd /gn_auth | |
| parent | 4a8ffa773ad487d1e0ad96ded074f4efcf8a438c (diff) | |
| download | gn-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.py | 19 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/views.py | 27 |
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) |
