"""Tests for admin role-assignment HTTP endpoints.""" import pytest from gn_auth.auth.db import sqlite3 as db 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 # Body used in all role-assign tests — assigning system-administrator on the # system resource is a real, migrations-seeded combination. _ASSIGN_BODY = { "role_name": "system-administrator", "resource_id": str(SYSTEM_RESOURCE.resource_id) } # Target user for assignment: unaff@iliated.user (no roles initially) _TARGET_USER = conftest.TEST_USERS[3] def _setup_admin_mock(conn, clients, mocker): """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: 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( admin, tuple(c for c in clients if c.user == admin)[0])) return admin @pytest.mark.unit_test def test_assign_role_no_token_returns_401(fxtr_app): """ GIVEN: no Authorization header WHEN: POST /auth/user//roles/assign THEN: 401 is returned """ with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/assign", json=_ASSIGN_BODY) assert res.status_code == 401 @pytest.mark.unit_test def test_assign_role_non_admin_returns_403(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid token belonging to a non-admin user WHEN: POST /auth/user//roles/assign THEN: 403 is returned """ _conn, clients = fxtr_oauth2_clients user = conftest.TEST_USERS[3] # unaff@iliated.user — no privileges mocker.patch( "gn_auth.auth.authorisation.users.views.require_oauth.acquire", conftest.get_tokeniser( user, tuple(c for c in clients if c.user == user)[0])) with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/assign", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) assert res.status_code == 403 def _revoke_assigned_role(conn): """Remove the role row written by the success tests. Keeps the DB in the state the fixtures expect — no user_roles entry for _TARGET_USER — so teardown and any subsequent queries are not surprised. """ with db.cursor(conn) as cursor: cursor.execute( "DELETE FROM user_roles " "WHERE user_id=? " "AND role_id=(SELECT role_id FROM roles WHERE role_name=?) " "AND resource_id=?", (str(_TARGET_USER.user_id), _ASSIGN_BODY["role_name"], _ASSIGN_BODY["resource_id"])) @pytest.mark.unit_test def test_assign_role_admin_returns_200(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid system-admin token and a valid role/resource body WHEN: POST /auth/user//roles/assign THEN: 200 is returned """ conn, clients = fxtr_oauth2_clients _setup_admin_mock(conn, clients, mocker) try: with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/assign", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) assert res.status_code == 200 finally: _revoke_assigned_role(conn) def _assign_target_role(conn): """Pre-assign system-administrator to _TARGET_USER on SYSTEM_RESOURCE. Required setup for revoke tests: the endpoint can only revoke what exists. """ with db.cursor(conn) as cursor: assign_user_role_by_name( cursor, _TARGET_USER, SYSTEM_RESOURCE.resource_id, _ASSIGN_BODY["role_name"]) def _cleanup_target_role(conn): """Remove _TARGET_USER's system-administrator row if still present. No-op when the revoke endpoint already deleted it; guards against test failures that leave the DB dirty. """ with db.cursor(conn) as cursor: cursor.execute( "DELETE FROM user_roles " "WHERE user_id=? " "AND role_id=(SELECT role_id FROM roles WHERE role_name=?) " "AND resource_id=?", (str(_TARGET_USER.user_id), _ASSIGN_BODY["role_name"], _ASSIGN_BODY["resource_id"])) @pytest.mark.unit_test def test_assign_role_persists_to_db(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid system-admin token and a valid role/resource body WHEN: POST /auth/user//roles/assign THEN: the user_roles row is present in the DB for that user/role/resource """ conn, clients = fxtr_oauth2_clients _setup_admin_mock(conn, clients, mocker) try: with fxtr_app.test_client() as http: http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/assign", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) with db.cursor(conn) as cursor: cursor.execute( "SELECT COUNT(*) AS cnt FROM user_roles " "INNER JOIN roles ON user_roles.role_id=roles.role_id " "WHERE user_roles.user_id=? " "AND roles.role_name=? " "AND user_roles.resource_id=?", (str(_TARGET_USER.user_id), _ASSIGN_BODY["role_name"], _ASSIGN_BODY["resource_id"])) assert cursor.fetchone()["cnt"] == 1 finally: _revoke_assigned_role(conn) # --------------------------------------------------------------------------- # HTTP endpoint tests: POST /auth/user//roles/revoke # --------------------------------------------------------------------------- @pytest.mark.unit_test def test_revoke_role_no_token_returns_401(fxtr_app): """ GIVEN: no Authorization header WHEN: POST /auth/user//roles/revoke THEN: 401 is returned """ with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", json=_ASSIGN_BODY) assert res.status_code == 401 @pytest.mark.unit_test def test_revoke_role_non_admin_returns_403(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid token belonging to a non-admin user WHEN: POST /auth/user//roles/revoke THEN: 403 is returned """ _conn, clients = fxtr_oauth2_clients user = conftest.TEST_USERS[3] # unaff@iliated.user — no privileges mocker.patch( "gn_auth.auth.authorisation.users.views.require_oauth.acquire", conftest.get_tokeniser( user, tuple(c for c in clients if c.user == user)[0])) with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) assert res.status_code == 403 @pytest.mark.unit_test def test_revoke_role_admin_returns_200(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid token with resource:user:assign-role and the target user holds the role on the resource WHEN: POST /auth/user//roles/revoke THEN: 200 is returned """ conn, clients = fxtr_oauth2_clients _setup_admin_mock(conn, clients, mocker) _assign_target_role(conn) try: with fxtr_app.test_client() as http: res = http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) assert res.status_code == 200 finally: _cleanup_target_role(conn) @pytest.mark.unit_test def test_revoke_role_removes_from_db(fxtr_app, mocker, fxtr_oauth2_clients): """ GIVEN: a valid token with resource:user:assign-role and the target user holds the role on the resource WHEN: POST /auth/user//roles/revoke THEN: the user_roles row is absent from the DB """ conn, clients = fxtr_oauth2_clients _setup_admin_mock(conn, clients, mocker) _assign_target_role(conn) try: with fxtr_app.test_client() as http: http.post( f"/auth/user/{_TARGET_USER.user_id}/roles/revoke", json=_ASSIGN_BODY, headers={"Authorization": "Bearer some-mocked-token"}) with db.cursor(conn) as cursor: cursor.execute( "SELECT COUNT(*) AS cnt FROM user_roles " "INNER JOIN roles ON user_roles.role_id=roles.role_id " "WHERE user_roles.user_id=? " "AND roles.role_name=? " "AND user_roles.resource_id=?", (str(_TARGET_USER.user_id), _ASSIGN_BODY["role_name"], _ASSIGN_BODY["resource_id"])) assert cursor.fetchone()["cnt"] == 0 finally: _cleanup_target_role(conn)