about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--gn_auth/auth/authorisation/users/views.py29
-rw-r--r--tests/unit/auth/test_admin_user_roles.py12
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(