about summary refs log tree commit diff
path: root/gn3/auth
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 /gn3/auth
parent6c76667857d5bbc8db962a551cece3f068074055 (diff)
downloadgenenetwork3-30da2f48eb35360bb339d54da2ab83d96a1cf85b.tar.gz
auth: resource: Enable viewing the details of a resource.
Diffstat (limited to 'gn3/auth')
-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
4 files changed, 143 insertions, 16 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)))