From 30da2f48eb35360bb339d54da2ab83d96a1cf85b Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Mon, 6 Feb 2023 14:20:24 +0300 Subject: auth: resource: Enable viewing the details of a resource. --- gn3/auth/authorisation/groups/models.py | 20 ++++++++- gn3/auth/authorisation/resources/checks.py | 47 +++++++++++++++++++ gn3/auth/authorisation/resources/models.py | 72 +++++++++++++++++++++++++----- gn3/auth/authorisation/resources/views.py | 20 ++++++--- 4 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 gn3/auth/authorisation/resources/checks.py (limited to 'gn3/auth') diff --git a/gn3/auth/authorisation/groups/models.py b/gn3/auth/authorisation/groups/models.py index c5c9370..49b5066 100644 --- a/gn3/auth/authorisation/groups/models.py +++ b/gn3/auth/authorisation/groups/models.py @@ -12,7 +12,7 @@ from gn3.auth.authentication.users import User from ..checks import authorised_p from ..privileges import Privilege -from ..errors import AuthorisationError +from ..errors import NotFoundError, AuthorisationError from ..roles.models import ( Role, create_role, revoke_user_role_by_name, assign_user_role_by_name) @@ -224,3 +224,21 @@ def group_users(conn: db.DbConnection, group_id: UUID) -> Iterable[User]: return (User(UUID(row["user_id"]), row["email"], row["name"]) for row in results) + +@authorised_p( + privileges = ("system:group:view-group",), + error_description = ( + "You do not have the appropriate privileges to access the group.")) +def group_by_id(conn: db.DbConnection, group_id: UUID) -> Group: + """Retrieve a group by its ID""" + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM groups WHERE group_id=:group_id", + {"group_id": str(group_id)}) + row = cursor.fetchone() + if row: + return Group( + UUID(row["group_id"]), + row["group_name"], + json.loads(row["group_metadata"])) + + raise NotFoundError(f"Could not find group with ID '{group_id}'.") diff --git a/gn3/auth/authorisation/resources/checks.py b/gn3/auth/authorisation/resources/checks.py new file mode 100644 index 0000000..fafde76 --- /dev/null +++ b/gn3/auth/authorisation/resources/checks.py @@ -0,0 +1,47 @@ +"""Handle authorisation checks for resources""" +from uuid import UUID +from functools import reduce +from typing import Sequence + +from gn3.auth import db +from gn3.auth.authentication.users import User + +def __organise_privileges_by_resource_id__(rows): + def __organise__(privs, row): + resource_id = UUID(row["resource_id"]) + return { + **privs, + resource_id: (row["privilege_id"],) + privs.get( + resource_id, tuple()) + } + return reduce(__organise__, rows, {}) + +def authorised_for(conn: db.DbConnection, user: User, privileges: tuple[str], + resource_ids: Sequence[UUID]) -> dict[UUID, bool]: + """ + Check whether `user` is authorised to access `resources` according to given + `privileges`. + """ + with db.cursor(conn) as cursor: + cursor.execute( + ("SELECT guror.*, rp.privilege_id FROM " + "group_user_roles_on_resources AS guror " + "INNER JOIN group_roles AS gr ON " + "(guror.group_id=gr.group_id AND guror.role_id=gr.role_id) " + "INNER JOIN roles AS r ON gr.role_id=r.role_id " + "INNER JOIN role_privileges AS rp ON r.role_id=rp.role_id " + "WHERE guror.user_id=? " + f"AND guror.resource_id IN ({', '.join(['?']*len(resource_ids))})" + f"AND rp.privilege_id IN ({', '.join(['?']*len(privileges))})"), + ((str(user.user_id),) + tuple( + str(r_id) for r_id in resource_ids) + tuple(privileges))) + resource_privileges = __organise_privileges_by_resource_id__( + cursor.fetchall()) + authorised = tuple(resource_id for resource_id, res_privileges + in resource_privileges.items() + if all(priv in res_privileges + for priv in privileges)) + return { + resource_id: resource_id in authorised + for resource_id in resource_ids + } diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py index df7fdf9..368ac1b 100644 --- a/gn3/auth/authorisation/resources/models.py +++ b/gn3/auth/authorisation/resources/models.py @@ -3,15 +3,15 @@ import json from uuid import UUID, uuid4 from typing import Any, Dict, Sequence, NamedTuple -from pymonad.maybe import Just, Maybe, Nothing - from gn3.auth import db from gn3.auth.dictify import dictify from gn3.auth.authentication.users import User +from .checks import authorised_for + from ..checks import authorised_p -from ..errors import AuthorisationError -from ..groups.models import Group, user_group, is_group_leader +from ..errors import NotFoundError, AuthorisationError +from ..groups.models import Group, user_group, group_by_id, is_group_leader class MissingGroupError(AuthorisationError): """Raised for any resource operation without a group.""" @@ -47,6 +47,32 @@ class Resource(NamedTuple): "public": self.public } +def __assign_resource_owner_role__(cursor, resource, user): + """Assign `user` the 'Resource Owner' role for `resource`.""" + cursor.execute( + "SELECT gr.* FROM group_roles AS gr INNER JOIN roles AS r " + "ON gr.role_id=r.role_id WHERE r.role_name='resource-owner'") + role = cursor.fetchone() + if not role: + cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'") + role = cursor.fetchone() + cursor.execute( + "INSERT INTO group_roles VALUES " + "(:group_role_id, :group_id, :role_id)", + {"group_role_id": str(uuid4()), + "group_id": str(resource.group.group_id), + "role_id": role["role_id"]}) + + cursor.execute( + "INSERT INTO group_user_roles_on_resources " + "VALUES (" + ":group_id, :user_id, :role_id, :resource_id" + ")", + {"group_id": str(resource.group.group_id), + "user_id": str(user.user_id), + "role_id": role["role_id"], + "resource_id": str(resource.resource_id)}) + @authorised_p(("group:resource:create-resource",), error_description="Insufficient privileges to create a resource", oauth2_scope="profile resource") @@ -67,12 +93,12 @@ def create_resource( resource_name, str(resource.resource_category.resource_category_id), 1 if resource.public else 0)) - # assign_resource_owner_role(conn, resource, user) + __assign_resource_owner_role__(cursor, resource, user) return resource def resource_category_by_id( - conn: db.DbConnection, category_id: UUID) -> Maybe[ResourceCategory]: + conn: db.DbConnection, category_id: UUID) -> ResourceCategory: """Retrieve a resource category by its ID.""" with db.cursor(conn) as cursor: cursor.execute( @@ -81,12 +107,13 @@ def resource_category_by_id( (str(category_id),)) results = cursor.fetchone() if results: - return Just(ResourceCategory( + return ResourceCategory( UUID(results["resource_category_id"]), results["resource_category_key"], - results["resource_category_description"])) + results["resource_category_description"]) - return Nothing + raise NotFoundError( + f"Could not find a ResourceCategory with ID '{category_id}'") def resource_categories(conn: db.DbConnection) -> Sequence[ResourceCategory]: """Retrieve all available resource categories""" @@ -148,10 +175,11 @@ def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: "WHERE group_user_roles_on_resources.group_id = ? " "AND group_user_roles_on_resources.user_id = ?"), (str(group.group_id), str(user.user_id))) + rows = cursor.fetchall() private_res = tuple( Resource(group, UUID(row[1]), row[2], categories[UUID(row[3])], bool(row[4])) - for row in cursor.fetchall()) + for row in rows) return tuple({ res.resource_id: res for res in @@ -161,3 +189,27 @@ def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]: # Fix the typing here return user_group(cursor, user).map(__all_resources__).maybe(# type: ignore[arg-type,misc] public_resources(conn), lambda res: res)# type: ignore[arg-type,return-value] + +def resource_by_id( + conn: db.DbConnection, user: User, resource_id: UUID) -> Resource: + """Retrieve a resource by its ID.""" + if not authorised_for( + conn, user, ("group:resource:view-resource",), + (resource_id,))[resource_id]: + raise AuthorisationError( + "You are not authorised to access resource with id " + f"'{resource_id}'.") + + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM resources WHERE resource_id=:id", + {"id": str(resource_id)}) + row = cursor.fetchone() + if row: + return Resource( + group_by_id(conn, UUID(row["group_id"])), + UUID(row["resource_id"]), + row["resource_name"], + resource_category_by_id(conn, row["resource_category_id"]), + bool(int(row["public"]))) + + raise NotFoundError(f"Could not find a resource with id '{resource_id}'") diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py index 77346bb..b45a9fc 100644 --- a/gn3/auth/authorisation/resources/views.py +++ b/gn3/auth/authorisation/resources/views.py @@ -1,9 +1,9 @@ """The views/routes for the resources package""" import uuid -from flask import request, jsonify, Blueprint, current_app as app +from flask import request, jsonify, Response, Blueprint, current_app as app from .models import ( - resource_categories, resource_category_by_id, + resource_by_id, resource_categories, resource_category_by_id, create_resource as _create_resource) from ... import db @@ -14,7 +14,7 @@ resources = Blueprint("resources", __name__) @resources.route("/categories", methods=["GET"]) @require_oauth("profile group resource") -def list_resource_categories(): +def list_resource_categories() -> Response: """Retrieve all resource categories""" db_uri = app.config["AUTH_DB"] with db.connection(db_uri) as conn: @@ -23,7 +23,7 @@ def list_resource_categories(): @resources.route("/create", methods=["POST"]) @require_oauth("profile group resource") -def create_resource(): +def create_resource() -> Response: """Create a new resource""" with require_oauth.acquire("profile group resource") as the_token: form = request.form @@ -33,6 +33,16 @@ def create_resource(): with db.connection(db_uri) as conn: resource = _create_resource( conn, resource_name, resource_category_by_id( - conn, resource_category_id).maybe(False, lambda rcat: rcat), + conn, resource_category_id), the_token.user) return jsonify(dictify(resource)) + +@resources.route("/view/") +@require_oauth("profile group resource") +def view_resource(resource_id: uuid.UUID) -> Response: + """View a particular resource's details.""" + with require_oauth.acquire("profile group resource") as the_token: + db_uri = app.config["AUTH_DB"] + with db.connection(db_uri) as conn: + return jsonify(dictify(resource_by_id( + conn, the_token.user, resource_id))) -- cgit v1.2.3