diff options
| author | Claude Sonnet 4.6 | 2026-08-27 14:44:56 +0000 |
|---|---|---|
| committer | Frederick Muriuki Muriithi | 2026-08-27 10:17:50 -0500 |
| commit | 0e21fb99310dfdd776e9b974572ee231d8f8c75b (patch) | |
| tree | 03f65e823c45faa735cd9f5968e83eb9c081722a | |
| parent | 522865ba4e792b8ae0ed4b4dc5c65be821a03359 (diff) | |
| download | gn-auth-0e21fb99310dfdd776e9b974572ee231d8f8c75b.tar.gz | |
feat(users/admin): implement POST /auth/user/<uid>/roles/assign
Checks resource:user:assign-role via can_assign_role (gn_libs.privileges.resources) on the caller's roles for the request's resource_id — caller must hold resource-owner (or masquerade as one) on that resource. Updates test setup to grant resource-owner on SYSTEM_RESOURCE instead of system-administrator, matching the actual privilege model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Reviewed-By: Frederick M. Muriithi <fredmanglis@gmail.com>
| -rw-r--r-- | gn_auth/auth/authorisation/users/views.py | 29 | ||||
| -rw-r--r-- | tests/unit/auth/test_admin_user_roles.py | 12 |
2 files changed, 37 insertions, 4 deletions
diff --git a/gn_auth/auth/authorisation/users/views.py b/gn_auth/auth/authorisation/users/views.py index cfc6720..ae33630 100644 --- a/gn_auth/auth/authorisation/users/views.py +++ b/gn_auth/auth/authorisation/users/views.py @@ -24,6 +24,7 @@ from flask import ( render_template) from gn_libs.privileges.system import can_create_or_delete_user +from gn_libs.privileges.resources import can_assign_role from gn_auth.smtp import send_message, build_email_message @@ -40,7 +41,8 @@ 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, user_roles as _user_roles) + assign_default_roles, assign_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) @@ -800,3 +802,28 @@ def create_new_user() -> Response: "email": user.email, "name": user.name }), 201) + + +@users.route("/<uuid:user_id>/roles/assign", methods=["POST"]) +def assign_user_role(user_id: uuid.UUID) -> Response: + """Assign a role to 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: + assign_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) diff --git a/tests/unit/auth/test_admin_user_roles.py b/tests/unit/auth/test_admin_user_roles.py index c751e20..6c153b5 100644 --- a/tests/unit/auth/test_admin_user_roles.py +++ b/tests/unit/auth/test_admin_user_roles.py @@ -2,7 +2,7 @@ import pytest from gn_auth.auth.db import sqlite3 as db -from gn_auth.auth.authorisation.users.admin.models import grant_sysadmin_role +from gn_auth.auth.authorisation.roles.models import assign_user_role_by_name from tests.unit.auth import conftest from tests.unit.auth.fixtures.resource_fixtures import SYSTEM_RESOURCE @@ -19,10 +19,16 @@ _TARGET_USER = conftest.TEST_USERS[3] def _setup_admin_mock(conn, clients, mocker): - """Grant sysadmin role and mock the token for sys@admin.user.""" + """Grant resource-owner role on SYSTEM_RESOURCE and mock the token. + + resource-owner carries resource:user:assign-role, which is what the + endpoint checks. In production the caller would masquerade as the + resource owner; here we grant the role directly for test setup. + """ admin = conftest.TEST_USERS[4] with db.cursor(conn) as cursor: - grant_sysadmin_role(cursor, admin) + assign_user_role_by_name( + cursor, admin, SYSTEM_RESOURCE.resource_id, "resource-owner") mocker.patch( "gn_auth.auth.authorisation.users.views.require_oauth.acquire", conftest.get_tokeniser( |
