From 0a31f61ee9db84eb35087073ef6b58f352252aae Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Tue, 3 Jan 2023 07:22:02 +0300 Subject: auth: Fetch all of a user's roles. * gn3/auth/authorisation/roles.py: Fetch roles from DB * gn3/auth/authorisation/views.py: Provide API endpoint for user roles * tests/unit/auth/test_roles.py: Tests to check fetching roles works correctly Fix linting and typing issues in the following files: * gn3/auth/authentication/oauth2/resource_server.py * gn3/auth/authentication/oauth2/views.py * tests/unit/auth/fixtures/oauth2_client_fixtures.py --- gn3/auth/authentication/oauth2/resource_server.py | 4 +- gn3/auth/authentication/oauth2/views.py | 5 +- gn3/auth/authorisation/roles.py | 47 ++++++++++++++++ gn3/auth/authorisation/views.py | 15 +++++ tests/unit/auth/fixtures/oauth2_client_fixtures.py | 1 + tests/unit/auth/test_roles.py | 65 +++++++++++++++++++++- 6 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 gn3/auth/authorisation/views.py diff --git a/gn3/auth/authentication/oauth2/resource_server.py b/gn3/auth/authentication/oauth2/resource_server.py index 885cbd8..223e811 100644 --- a/gn3/auth/authentication/oauth2/resource_server.py +++ b/gn3/auth/authentication/oauth2/resource_server.py @@ -2,7 +2,7 @@ from flask import current_app as app from authlib.oauth2.rfc6750 import BearerTokenValidator as _BearerTokenValidator -from authlib.integrations.flask_oauth2 import ResourceProtector, current_token +from authlib.integrations.flask_oauth2 import ResourceProtector from gn3.auth import db from gn3.auth.authentication.oauth2.models.oauth2token import token_by_access_token @@ -11,7 +11,7 @@ class BearerTokenValidator(_BearerTokenValidator): """Extends `authlib.oauth2.rfc6750.BearerTokenValidator`""" def authenticate_token(self, token_string: str): with db.connection(app.config["AUTH_DB"]) as conn: - return token_by_access_token(conn, token_string).maybe( + return token_by_access_token(conn, token_string).maybe(# type: ignore[misc] None, lambda tok: tok) require_oauth = ResourceProtector() diff --git a/gn3/auth/authentication/oauth2/views.py b/gn3/auth/authentication/oauth2/views.py index 0947aa2..7d0d7dd 100644 --- a/gn3/auth/authentication/oauth2/views.py +++ b/gn3/auth/authentication/oauth2/views.py @@ -45,8 +45,9 @@ def introspect_token(): @oauth2.route("/user") @require_oauth("profile") def user_details(): - with require_oauth.acquire("profile") as token: - user = token.user + """Return user's details.""" + with require_oauth.acquire("profile") as the_token: + user = the_token.user return jsonify({ "user_id": user.user_id, "email": user.email, diff --git a/gn3/auth/authorisation/roles.py b/gn3/auth/authorisation/roles.py index 397ad80..e71d427 100644 --- a/gn3/auth/authorisation/roles.py +++ b/gn3/auth/authorisation/roles.py @@ -1,8 +1,10 @@ """Handle management of roles""" from uuid import UUID, uuid4 +from functools import reduce from typing import Iterable, NamedTuple from gn3.auth import db +from gn3.auth.authentication.users import User from gn3.auth.authentication.checks import authenticated_p from .checks import authorised_p @@ -42,3 +44,48 @@ def create_role( for priv in privileges)) return role + +def __organise_privileges__(roles_dict, privilege_row): + """Organise the privileges into their roles.""" + role_id_str = privilege_row["role_id"] + if role_id_str in roles_dict: + return { + **roles_dict, + role_id_str: Role( + UUID(role_id_str), + privilege_row["role_name"], + roles_dict[role_id_str].privileges + ( + Privilege(UUID(privilege_row["privilege_id"]), + privilege_row["privilege_name"]),)) + } + + return { + **roles_dict, + role_id_str: Role( + UUID(role_id_str), + privilege_row["role_name"], + (Privilege(UUID(privilege_row["privilege_id"]), + privilege_row["privilege_name"]),)) + } + +def user_roles(conn: db.DbConnection, user: User): + """Retrieve ALL roles assigned to the user.""" + with db.cursor(conn) as cursor: + cursor.execute( + "SELECT r.*, p.* FROM user_roles AS ur INNER JOIN roles AS r " + "ON ur.role_id=r.role_id INNER JOIN role_privileges AS rp " + "ON r.role_id=rp.role_id INNER JOIN privileges AS p " + "ON rp.privilege_id=p.privilege_id WHERE ur.user_id=? " + "UNION " + "SELECT r.*, p.* FROM group_user_roles_on_resources AS guror " + "INNER JOIN roles AS r ON guror.role_id=r.role_id " + "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id " + "INNER JOIN privileges AS p ON rp.privilege_id=p.privilege_id " + "WHERE guror.user_id=?", + ((str(user.user_id),)*2)) + + results = cursor.fetchall() + if results: + return tuple( + reduce(__organise_privileges__, results, {}).values()) + return tuple() diff --git a/gn3/auth/authorisation/views.py b/gn3/auth/authorisation/views.py new file mode 100644 index 0000000..2481633 --- /dev/null +++ b/gn3/auth/authorisation/views.py @@ -0,0 +1,15 @@ +"""Endpoints for the authorisation stuff.""" +from flask import jsonify, current_app + +from gn3.auth import db +from .roles import user_roles as _user_roles +from ..authentication.oauth2.views import oauth2 +from ..authentication.oauth2.resource_server import require_oauth + +@oauth2.route("/user-roles") +@require_oauth +def user_roles(): + """Return the roles assigned to the user.""" + with require_oauth.acquire("role") as token: + with db.connection(current_app.config["AUTH_DB"]) as conn: + return jsonify(_user_roles(conn, token.user)) diff --git a/tests/unit/auth/fixtures/oauth2_client_fixtures.py b/tests/unit/auth/fixtures/oauth2_client_fixtures.py index 5f11e92..040da87 100644 --- a/tests/unit/auth/fixtures/oauth2_client_fixtures.py +++ b/tests/unit/auth/fixtures/oauth2_client_fixtures.py @@ -10,6 +10,7 @@ from gn3.auth.authentication.oauth2.models.oauth2client import OAuth2Client @pytest.fixture(autouse=True) def fxtr_patch_envvars(monkeypatch): + """Fixture: patch environment variable""" monkeypatch.setenv("AUTHLIB_INSECURE_TRANSPORT", "true") @pytest.fixture diff --git a/tests/unit/auth/test_roles.py b/tests/unit/auth/test_roles.py index 92384bb..3fc146a 100644 --- a/tests/unit/auth/test_roles.py +++ b/tests/unit/auth/test_roles.py @@ -5,9 +5,10 @@ import pytest from gn3.auth import db from gn3.auth.authorisation.privileges import Privilege -from gn3.auth.authorisation.roles import Role, create_role +from gn3.auth.authorisation.roles import Role, user_roles, create_role from tests.unit.auth import conftest +from tests.unit.auth.fixtures import TEST_USERS create_role_failure = { "status": "error", @@ -43,3 +44,65 @@ def test_create_role(# pylint: disable=[too-many-arguments] with db.connection(auth_testdb_path) as conn, db.cursor(conn) as cursor: the_role = create_role(cursor, "a_test_role", PRIVILEGES) assert the_role == expected + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "user,expected", + (zip(TEST_USERS, + ((Role( + role_id=uuid.UUID('a0e67630-d502-4b9f-b23f-6805d0f30e30'), + role_name='group-leader', + privileges=( + Privilege( + privilege_id=uuid.UUID('13ec2a94-4f1a-442d-aad2-936ad6dd5c57'), + privilege_name='delete-group'), + Privilege( + privilege_id=uuid.UUID('1c59eff5-9336-4ed2-a166-8f70d4cb012e'), + privilege_name='delete-role'), + Privilege( + privilege_id=uuid.UUID('221660b1-df05-4be1-b639-f010269dbda9'), + privilege_name='create-role'), + Privilege( + privilege_id=uuid.UUID('2f980855-959b-4339-b80e-25d1ec286e21'), + privilege_name='edit-resource'), + Privilege( + privilege_id=uuid.UUID('3ebfe79c-d159-4629-8b38-772cf4bc2261'), + privilege_name='view-group'), + Privilege( + privilege_id=uuid.UUID('4842e2aa-38b9-4349-805e-0a99a9cf8bff'), + privilege_name='create-group'), + Privilege( + privilege_id=uuid.UUID('5103cc68-96f8-4ebb-83a4-a31692402c9b'), + privilege_name='assign-role'), + Privilege( + privilege_id=uuid.UUID('52576370-b3c7-4e6a-9f7e-90e9dbe24d8f'), + privilege_name='edit-group'), + Privilege( + privilege_id=uuid.UUID('7bcca363-cba9-4169-9e31-26bdc6179b28'), + privilege_name='edit-role'), + Privilege( + privilege_id=uuid.UUID('7f261757-3211-4f28-a43f-a09b800b164d'), + privilege_name='view-resource'), + Privilege( + privilege_id=uuid.UUID('aa25b32a-bff2-418d-b0a2-e26b4a8f089b'), + privilege_name='create-resource'), + Privilege( + privilege_id=uuid.UUID('ae4add8c-789a-4d11-a6e9-a306470d83d9'), + privilege_name='add-group-member'), + Privilege( + privilege_id=uuid.UUID('d2a070fd-e031-42fb-ba41-d60cf19e5d6d'), + privilege_name='delete-resource'), + Privilege( + privilege_id=uuid.UUID('d4afe2b3-4ca0-4edd-b37d-966535b5e5bd'), + privilege_name='transfer-group-leadership'), + Privilege( + privilege_id=uuid.UUID('f1bd3f42-567e-4965-9643-6d1a52ddee64'), + privilege_name='remove-group-member'))),), + tuple(), tuple(), tuple())))) +def test_user_roles(fxtr_group_user_roles, user, expected): + """ + GIVEN: an authenticated user + WHEN: we request the user's privileges + THEN: return **ALL** the privileges attached to the user + """ + assert user_roles(fxtr_group_user_roles, user) == expected -- cgit v1.2.3