aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-02-06 14:20:24 +0300
committerFrederick Muriuki Muriithi2023-02-06 14:20:24 +0300
commit30da2f48eb35360bb339d54da2ab83d96a1cf85b (patch)
treee30f2e0c3e884df0be52c7c3ffe65d1636aaa2c1
parent6c76667857d5bbc8db962a551cece3f068074055 (diff)
downloadgenenetwork3-30da2f48eb35360bb339d54da2ab83d96a1cf85b.tar.gz
auth: resource: Enable viewing the details of a resource.
-rw-r--r--gn3/auth/authorisation/groups/models.py20
-rw-r--r--gn3/auth/authorisation/resources/checks.py47
-rw-r--r--gn3/auth/authorisation/resources/models.py72
-rw-r--r--gn3/auth/authorisation/resources/views.py20
-rw-r--r--migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py11
-rw-r--r--tests/unit/auth/test_migrations_insert_data_into_empty_table.py4
-rw-r--r--tests/unit/auth/test_resources.py13
7 files changed, 165 insertions, 22 deletions
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/<uuid:resource_id>")
+@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)))
diff --git a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py b/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
index c4887cd..386f481 100644
--- a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
+++ b/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
@@ -10,7 +10,8 @@ steps = [
step(
"""
INSERT INTO roles(role_id, role_name, user_editable) VALUES
- ('a0e67630-d502-4b9f-b23f-6805d0f30e30', 'group-leader', '0')
+ ('a0e67630-d502-4b9f-b23f-6805d0f30e30', 'group-leader', '0'),
+ ('522e4d40-aefc-4a64-b7e0-768b8be517ee', 'resource-owner', '0')
""",
"DELETE FROM roles"),
step(
@@ -41,6 +42,14 @@ steps = [
('a0e67630-d502-4b9f-b23f-6805d0f30e30',
'2f980855-959b-4339-b80e-25d1ec286e21'),
('a0e67630-d502-4b9f-b23f-6805d0f30e30',
+ 'd2a070fd-e031-42fb-ba41-d60cf19e5d6d'),
+ ('522e4d40-aefc-4a64-b7e0-768b8be517ee',
+ 'aa25b32a-bff2-418d-b0a2-e26b4a8f089b'),
+ ('522e4d40-aefc-4a64-b7e0-768b8be517ee',
+ '7f261757-3211-4f28-a43f-a09b800b164d'),
+ ('522e4d40-aefc-4a64-b7e0-768b8be517ee',
+ '2f980855-959b-4339-b80e-25d1ec286e21'),
+ ('522e4d40-aefc-4a64-b7e0-768b8be517ee',
'd2a070fd-e031-42fb-ba41-d60cf19e5d6d')
""",
"DELETE FROM role_privileges")
diff --git a/tests/unit/auth/test_migrations_insert_data_into_empty_table.py b/tests/unit/auth/test_migrations_insert_data_into_empty_table.py
index b4845a0..ebb7fa6 100644
--- a/tests/unit/auth/test_migrations_insert_data_into_empty_table.py
+++ b/tests/unit/auth/test_migrations_insert_data_into_empty_table.py
@@ -10,8 +10,8 @@ from tests.unit.auth.conftest import (
test_params = (
("20221113_01_7M0hv-enumerate-initial-privileges.py", "privileges", 19),
- ("20221114_04_tLUzB-initialise-basic-roles.py", "roles", 1),
- ("20221114_04_tLUzB-initialise-basic-roles.py", "role_privileges", 11))
+ ("20221114_04_tLUzB-initialise-basic-roles.py", "roles", 2),
+ ("20221114_04_tLUzB-initialise-basic-roles.py", "role_privileges", 15))
@pytest.mark.unit_test
@pytest.mark.parametrize(
diff --git a/tests/unit/auth/test_resources.py b/tests/unit/auth/test_resources.py
index 7e3d9ad..840c008 100644
--- a/tests/unit/auth/test_resources.py
+++ b/tests/unit/auth/test_resources.py
@@ -37,13 +37,20 @@ def test_create_resource(mocker, fxtr_users_in_group, user, expected):
mocker.patch("gn3.auth.authorisation.checks.require_oauth.acquire",
conftest.get_tokeniser(user))
conn, _group, _users = fxtr_users_in_group
- assert create_resource(
- conn, "test_resource", resource_category, user) == expected
+ resource = create_resource(conn, "test_resource", resource_category, user)
+ assert resource == expected
with db.cursor(conn) as cursor:
# Cleanup
cursor.execute(
- "DELETE FROM resources WHERE resource_id=?", (str(uuid_fn()),))
+ "DELETE FROM group_user_roles_on_resources WHERE resource_id=?",
+ (str(resource.resource_id),))
+ cursor.execute(
+ "DELETE FROM group_roles WHERE group_id=?",
+ (str(resource.group.group_id),))
+ cursor.execute(
+ "DELETE FROM resources WHERE resource_id=?",
+ (str(resource.resource_id),))
@pytest.mark.unit_test
@pytest.mark.parametrize(