about summary refs log tree commit diff
path: root/tests/unit/auth/test_admin_user_roles.py
blob: 7ce0b1fa71972b8eec36eda20ac8d976a65e1672 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""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/<uid>/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/<uid>/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/<uid>/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/<uid>/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/<uid>/roles/revoke
# ---------------------------------------------------------------------------

@pytest.mark.unit_test
def test_revoke_role_no_token_returns_401(fxtr_app):
    """
    GIVEN: no Authorization header
    WHEN: POST /auth/user/<uid>/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/<uid>/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/<uid>/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/<uid>/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)