about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation/resources
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-08-04 10:10:28 +0300
committerFrederick Muriuki Muriithi2023-08-04 10:20:09 +0300
commit8b7c598407a5fea9a3d78473e72df87606998cd4 (patch)
tree8526433a17eca6b511feb082a0574f9b15cb9469 /gn_auth/auth/authorisation/resources
parentf7fcbbcc014686ac597b783a8dcb38b43024b9d6 (diff)
downloadgn-auth-8b7c598407a5fea9a3d78473e72df87606998cd4.tar.gz
Copy over files from GN3 repository.
Diffstat (limited to 'gn_auth/auth/authorisation/resources')
-rw-r--r--gn_auth/auth/authorisation/resources/__init__.py2
-rw-r--r--gn_auth/auth/authorisation/resources/checks.py47
-rw-r--r--gn_auth/auth/authorisation/resources/models.py579
-rw-r--r--gn_auth/auth/authorisation/resources/views.py272
4 files changed, 900 insertions, 0 deletions
diff --git a/gn_auth/auth/authorisation/resources/__init__.py b/gn_auth/auth/authorisation/resources/__init__.py
new file mode 100644
index 0000000..869ab60
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/__init__.py
@@ -0,0 +1,2 @@
+"""Initialise the `gn3.auth.authorisation.resources` package."""
+from .models import Resource, ResourceCategory
diff --git a/gn_auth/auth/authorisation/resources/checks.py b/gn_auth/auth/authorisation/resources/checks.py
new file mode 100644
index 0000000..fafde76
--- /dev/null
+++ b/gn_auth/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/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py
new file mode 100644
index 0000000..b301a93
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/models.py
@@ -0,0 +1,579 @@
+"""Handle the management of resources."""
+import json
+import sqlite3
+from uuid import UUID, uuid4
+from functools import reduce, partial
+from typing import Any, Dict, Sequence, Optional, NamedTuple
+
+from gn3.auth import db
+from gn3.auth.dictify import dictify
+from gn3.auth.authentication.users import User
+from gn3.auth.db_utils import with_db_connection
+
+from .checks import authorised_for
+
+from ..checks import authorised_p
+from ..errors import NotFoundError, AuthorisationError
+from ..groups.models import (
+    Group, GroupRole, user_group, group_by_id, is_group_leader)
+
+class MissingGroupError(AuthorisationError):
+    """Raised for any resource operation without a group."""
+
+class ResourceCategory(NamedTuple):
+    """Class representing a resource category."""
+    resource_category_id: UUID
+    resource_category_key: str
+    resource_category_description: str
+
+    def dictify(self) -> dict[str, Any]:
+        """Return a dict representation of `ResourceCategory` objects."""
+        return {
+            "resource_category_id": self.resource_category_id,
+            "resource_category_key": self.resource_category_key,
+            "resource_category_description": self.resource_category_description
+        }
+
+class Resource(NamedTuple):
+    """Class representing a resource."""
+    group: Group
+    resource_id: UUID
+    resource_name: str
+    resource_category: ResourceCategory
+    public: bool
+    resource_data: Sequence[dict[str, Any]] = tuple()
+
+    def dictify(self) -> dict[str, Any]:
+        """Return a dict representation of `Resource` objects."""
+        return {
+            "group": dictify(self.group), "resource_id": self.resource_id,
+            "resource_name": self.resource_name,
+            "resource_category": dictify(self.resource_category),
+            "public": self.public,
+            "resource_data": self.resource_data
+        }
+
+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' "
+        "AND gr.group_id=?",
+        (str(resource.group.group_id),))
+    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")
+def create_resource(
+        conn: db.DbConnection, resource_name: str,
+        resource_category: ResourceCategory, user: User,
+        public: bool) -> Resource:
+    """Create a resource item."""
+    with db.cursor(conn) as cursor:
+        group = user_group(conn, user).maybe(
+            False, lambda grp: grp)# type: ignore[misc, arg-type]
+        if not group:
+            raise MissingGroupError(
+                "User with no group cannot create a resource.")
+        resource = Resource(
+            group, uuid4(), resource_name, resource_category, public)
+        cursor.execute(
+            "INSERT INTO resources VALUES (?, ?, ?, ?, ?)",
+            (str(resource.group.group_id), str(resource.resource_id),
+             resource_name,
+             str(resource.resource_category.resource_category_id),
+             1 if resource.public else 0))
+        __assign_resource_owner_role__(cursor, resource, user)
+
+    return resource
+
+def resource_category_by_id(
+        conn: db.DbConnection, category_id: UUID) -> ResourceCategory:
+    """Retrieve a resource category by its ID."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT * FROM resource_categories WHERE "
+            "resource_category_id=?",
+            (str(category_id),))
+        results = cursor.fetchone()
+        if results:
+            return ResourceCategory(
+                UUID(results["resource_category_id"]),
+                results["resource_category_key"],
+                results["resource_category_description"])
+
+    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"""
+    with db.cursor(conn) as cursor:
+        cursor.execute("SELECT * FROM resource_categories")
+        return tuple(
+            ResourceCategory(UUID(row[0]), row[1], row[2])
+            for row in cursor.fetchall())
+    return tuple()
+
+def public_resources(conn: db.DbConnection) -> Sequence[Resource]:
+    """List all resources marked as public"""
+    categories = {
+        str(cat.resource_category_id): cat for cat in resource_categories(conn)
+    }
+    with db.cursor(conn) as cursor:
+        cursor.execute("SELECT * FROM resources WHERE public=1")
+        results = cursor.fetchall()
+        group_uuids = tuple(row[0] for row in results)
+        query = ("SELECT * FROM groups WHERE group_id IN "
+                 f"({', '.join(['?'] * len(group_uuids))})")
+        cursor.execute(query, group_uuids)
+        groups = {
+            row[0]: Group(
+                UUID(row[0]), row[1], json.loads(row[2] or "{}"))
+            for row in cursor.fetchall()
+        }
+        return tuple(
+            Resource(groups[row[0]], UUID(row[1]), row[2], categories[row[3]],
+                     bool(row[4]))
+            for row in results)
+
+def group_leader_resources(
+        conn: db.DbConnection, user: User, group: Group,
+        res_categories: Dict[UUID, ResourceCategory]) -> Sequence[Resource]:
+    """Return all the resources available to the group leader"""
+    if is_group_leader(conn, user, group):
+        with db.cursor(conn) as cursor:
+            cursor.execute("SELECT * FROM resources WHERE group_id=?",
+                           (str(group.group_id),))
+            return tuple(
+                Resource(group, UUID(row[1]), row[2],
+                         res_categories[UUID(row[3])], bool(row[4]))
+                for row in cursor.fetchall())
+    return tuple()
+
+def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]:
+    """List the resources available to the user"""
+    categories = { # Repeated in `public_resources` function
+        cat.resource_category_id: cat for cat in resource_categories(conn)
+    }
+    with db.cursor(conn) as cursor:
+        def __all_resources__(group) -> Sequence[Resource]:
+            gl_resources = group_leader_resources(conn, user, group, categories)
+
+            cursor.execute(
+                ("SELECT resources.* FROM group_user_roles_on_resources "
+                 "LEFT JOIN resources "
+                 "ON group_user_roles_on_resources.resource_id=resources.resource_id "
+                 "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 rows)
+            return tuple({
+                res.resource_id: res
+                for res in
+                (private_res + gl_resources + public_resources(conn))# type: ignore[operator]
+            }.values())
+
+        # Fix the typing here
+        return user_group(conn, user).map(__all_resources__).maybe(# type: ignore[arg-type,misc]
+            public_resources(conn), lambda res: res)# type: ignore[arg-type,return-value]
+
+def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) -> tuple[dict, ...]:
+    """
+    Retrieve the data for `resource`, optionally limiting the number of items.
+    """
+    resource_data_function = {
+        "mrna": mrna_resource_data,
+        "genotype": genotype_resource_data,
+        "phenotype": phenotype_resource_data
+    }
+    with db.cursor(conn) as cursor:
+        return tuple(
+            dict(data_row) for data_row in
+            resource_data_function[
+                resource.resource_category.resource_category_key](
+                    cursor, resource.resource_id, offset, limit))
+
+def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource:
+    """Attach the linked data to the resource"""
+    resource_data_function = {
+        "mrna": mrna_resource_data,
+        "genotype": genotype_resource_data,
+        "phenotype": phenotype_resource_data
+    }
+    category = resource.resource_category
+    data_rows = tuple(
+        dict(data_row) for data_row in
+        resource_data_function[category.resource_category_key](
+            cursor, resource.resource_id))
+    return Resource(
+        resource.group, resource.resource_id, resource.resource_name,
+        resource.resource_category, resource.public, data_rows)
+
+def mrna_resource_data(cursor: db.DbCursor,
+                       resource_id: UUID,
+                       offset: int = 0,
+                       limit: Optional[int] = None) -> Sequence[sqlite3.Row]:
+    """Fetch data linked to a mRNA resource"""
+    cursor.execute(
+        (("SELECT * FROM mrna_resources AS mr "
+          "INNER JOIN linked_mrna_data AS lmr "
+          "ON mr.data_link_id=lmr.data_link_id "
+          "WHERE mr.resource_id=?") + (
+              f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")),
+        (str(resource_id),))
+    return cursor.fetchall()
+
+def genotype_resource_data(
+        cursor: db.DbCursor,
+        resource_id: UUID,
+        offset: int = 0,
+        limit: Optional[int] = None) -> Sequence[sqlite3.Row]:
+    """Fetch data linked to a Genotype resource"""
+    cursor.execute(
+        (("SELECT * FROM genotype_resources AS gr "
+          "INNER JOIN linked_genotype_data AS lgd "
+          "ON gr.data_link_id=lgd.data_link_id "
+          "WHERE gr.resource_id=?") + (
+              f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")),
+        (str(resource_id),))
+    return cursor.fetchall()
+
+def phenotype_resource_data(
+        cursor: db.DbCursor,
+        resource_id: UUID,
+        offset: int = 0,
+        limit: Optional[int] = None) -> Sequence[sqlite3.Row]:
+    """Fetch data linked to a Phenotype resource"""
+    cursor.execute(
+        ("SELECT * FROM phenotype_resources AS pr "
+         "INNER JOIN linked_phenotype_data AS lpd "
+         "ON pr.data_link_id=lpd.data_link_id "
+         "WHERE pr.resource_id=?") + (
+             f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""),
+        (str(resource_id),))
+    return cursor.fetchall()
+
+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}'")
+
+def __link_mrna_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Link mRNA Assay data with a resource."""
+    with db.cursor(conn) as cursor:
+        params = {
+            "group_id": str(resource.group.group_id),
+            "resource_id": str(resource.resource_id),
+            "data_link_id": str(data_link_id)
+        }
+        cursor.execute(
+            "INSERT INTO mrna_resources VALUES"
+            "(:group_id, :resource_id, :data_link_id)",
+            params)
+        return params
+
+def __link_geno_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Link Genotype data with a resource."""
+    with db.cursor(conn) as cursor:
+        params = {
+            "group_id": str(resource.group.group_id),
+            "resource_id": str(resource.resource_id),
+            "data_link_id": str(data_link_id)
+        }
+        cursor.execute(
+            "INSERT INTO genotype_resources VALUES"
+            "(:group_id, :resource_id, :data_link_id)",
+            params)
+        return params
+
+def __link_pheno_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Link Phenotype data with a resource."""
+    with db.cursor(conn) as cursor:
+        params = {
+            "group_id": str(resource.group.group_id),
+            "resource_id": str(resource.resource_id),
+            "data_link_id": str(data_link_id)
+        }
+        cursor.execute(
+            "INSERT INTO phenotype_resources VALUES"
+            "(:group_id, :resource_id, :data_link_id)",
+            params)
+        return params
+
+def link_data_to_resource(
+        conn: db.DbConnection, user: User, resource_id: UUID, dataset_type: str,
+        data_link_id: UUID) -> dict:
+    """Link data to resource."""
+    if not authorised_for(
+            conn, user, ("group:resource:edit-resource",),
+            (resource_id,))[resource_id]:
+        raise AuthorisationError(
+            "You are not authorised to link data to resource with id "
+            f"{resource_id}")
+
+    resource = with_db_connection(partial(
+        resource_by_id, user=user, resource_id=resource_id))
+    return {
+        "mrna": __link_mrna_data_to_resource__,
+        "genotype": __link_geno_data_to_resource__,
+        "phenotype": __link_pheno_data_to_resource__,
+    }[dataset_type.lower()](conn, resource, data_link_id)
+
+def __unlink_mrna_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Unlink data from mRNA Assay resources"""
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM mrna_resources "
+                       "WHERE resource_id=? AND data_link_id=?",
+                       (str(resource.resource_id), str(data_link_id)))
+        return {
+            "resource_id": str(resource.resource_id),
+            "dataset_type": resource.resource_category.resource_category_key,
+            "data_link_id": data_link_id
+        }
+
+def __unlink_geno_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Unlink data from Genotype resources"""
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM genotype_resources "
+                       "WHERE resource_id=? AND data_link_id=?",
+                       (str(resource.resource_id), str(data_link_id)))
+        return {
+            "resource_id": str(resource.resource_id),
+            "dataset_type": resource.resource_category.resource_category_key,
+            "data_link_id": data_link_id
+        }
+
+def __unlink_pheno_data_to_resource__(
+        conn: db.DbConnection, resource: Resource, data_link_id: UUID) -> dict:
+    """Unlink data from Phenotype resources"""
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM phenotype_resources "
+                       "WHERE resource_id=? AND data_link_id=?",
+                       (str(resource.resource_id), str(data_link_id)))
+        return {
+            "resource_id": str(resource.resource_id),
+            "dataset_type": resource.resource_category.resource_category_key,
+            "data_link_id": str(data_link_id)
+        }
+
+def unlink_data_from_resource(
+        conn: db.DbConnection, user: User, resource_id: UUID, data_link_id: UUID):
+    """Unlink data from resource."""
+    if not authorised_for(
+            conn, user, ("group:resource:edit-resource",),
+            (resource_id,))[resource_id]:
+        raise AuthorisationError(
+            "You are not authorised to link data to resource with id "
+            f"{resource_id}")
+
+    resource = with_db_connection(partial(
+        resource_by_id, user=user, resource_id=resource_id))
+    dataset_type = resource.resource_category.resource_category_key
+    return {
+        "mrna": __unlink_mrna_data_to_resource__,
+        "genotype": __unlink_geno_data_to_resource__,
+        "phenotype": __unlink_pheno_data_to_resource__,
+    }[dataset_type.lower()](conn, resource, data_link_id)
+
+def organise_resources_by_category(resources: Sequence[Resource]) -> dict[
+        ResourceCategory, tuple[Resource]]:
+    """Organise the `resources` by their categories."""
+    def __organise__(accumulator, resource):
+        category = resource.resource_category
+        return {
+            **accumulator,
+            category: accumulator.get(category, tuple()) + (resource,)
+        }
+    return reduce(__organise__, resources, {})
+
+def __attach_data__(
+        data_rows: Sequence[sqlite3.Row],
+        resources: Sequence[Resource]) -> Sequence[Resource]:
+    def __organise__(acc, row):
+        resource_id = UUID(row["resource_id"])
+        return {
+            **acc,
+            resource_id: acc.get(resource_id, tuple()) + (dict(row),)
+        }
+    organised: dict[UUID, tuple[dict, ...]] = reduce(__organise__, data_rows, {})
+    return tuple(
+        Resource(
+            resource.group, resource.resource_id, resource.resource_name,
+            resource.resource_category, resource.public,
+            organised.get(resource.resource_id, tuple()))
+        for resource in resources)
+
+def attach_mrna_resources_data(
+        cursor, resources: Sequence[Resource]) -> Sequence[Resource]:
+    """Attach linked data to mRNA Assay resources"""
+    placeholders = ", ".join(["?"] * len(resources))
+    cursor.execute(
+        "SELECT * FROM mrna_resources AS mr INNER JOIN linked_mrna_data AS lmd"
+        " ON mr.data_link_id=lmd.data_link_id "
+        f"WHERE mr.resource_id IN ({placeholders})",
+        tuple(str(resource.resource_id) for resource in resources))
+    return __attach_data__(cursor.fetchall(), resources)
+
+def attach_genotype_resources_data(
+        cursor, resources: Sequence[Resource]) -> Sequence[Resource]:
+    """Attach linked data to Genotype resources"""
+    placeholders = ", ".join(["?"] * len(resources))
+    cursor.execute(
+        "SELECT * FROM genotype_resources AS gr "
+        "INNER JOIN linked_genotype_data AS lgd "
+        "ON gr.data_link_id=lgd.data_link_id "
+        f"WHERE gr.resource_id IN ({placeholders})",
+        tuple(str(resource.resource_id) for resource in resources))
+    return __attach_data__(cursor.fetchall(), resources)
+
+def attach_phenotype_resources_data(
+        cursor, resources: Sequence[Resource]) -> Sequence[Resource]:
+    """Attach linked data to Phenotype resources"""
+    placeholders = ", ".join(["?"] * len(resources))
+    cursor.execute(
+        "SELECT * FROM phenotype_resources AS pr "
+        "INNER JOIN linked_phenotype_data AS lpd "
+        "ON pr.data_link_id=lpd.data_link_id "
+        f"WHERE pr.resource_id IN ({placeholders})",
+        tuple(str(resource.resource_id) for resource in resources))
+    return __attach_data__(cursor.fetchall(), resources)
+
+def attach_resources_data(
+        conn: db.DbConnection, resources: Sequence[Resource]) -> Sequence[
+            Resource]:
+    """Attach linked data for each resource in `resources`"""
+    resource_data_function = {
+        "mrna": attach_mrna_resources_data,
+        "genotype": attach_genotype_resources_data,
+        "phenotype": attach_phenotype_resources_data
+    }
+    organised = organise_resources_by_category(resources)
+    with db.cursor(conn) as cursor:
+        return tuple(
+            resource for categories in
+            (resource_data_function[category.resource_category_key](
+                cursor, rscs)
+             for category, rscs in organised.items())
+            for resource in categories)
+
+@authorised_p(
+    ("group:user:assign-role",),
+    "You cannot assign roles to users for this group.",
+    oauth2_scope="profile group role resource")
+def assign_resource_user(
+        conn: db.DbConnection, resource: Resource, user: User,
+        role: GroupRole) -> dict:
+    """Assign `role` to `user` for the specific `resource`."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "INSERT INTO "
+            "group_user_roles_on_resources(group_id, user_id, role_id, "
+            "resource_id) "
+            "VALUES (?, ?, ?, ?) "
+            "ON CONFLICT (group_id, user_id, role_id, resource_id) "
+            "DO NOTHING",
+            (str(resource.group.group_id), str(user.user_id),
+             str(role.role.role_id), str(resource.resource_id)))
+        return {
+            "resource": dictify(resource),
+            "user": dictify(user),
+            "role": dictify(role),
+            "description": (
+                f"The user '{user.name}'({user.email}) was assigned the "
+                f"'{role.role.role_name}' role on resource with ID "
+                f"'{resource.resource_id}'.")}
+
+@authorised_p(
+    ("group:user:assign-role",),
+    "You cannot assign roles to users for this group.",
+    oauth2_scope="profile group role resource")
+def unassign_resource_user(
+        conn: db.DbConnection, resource: Resource, user: User,
+        role: GroupRole) -> dict:
+    """Assign `role` to `user` for the specific `resource`."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "DELETE FROM group_user_roles_on_resources "
+            "WHERE group_id=? AND user_id=? AND role_id=? AND resource_id=?",
+            (str(resource.group.group_id), str(user.user_id),
+             str(role.role.role_id), str(resource.resource_id)))
+        return {
+            "resource": dictify(resource),
+            "user": dictify(user),
+            "role": dictify(role),
+            "description": (
+                f"The user '{user.name}'({user.email}) had the "
+                f"'{role.role.role_name}' role on resource with ID "
+                f"'{resource.resource_id}' taken away.")}
+
+def save_resource(
+        conn: db.DbConnection, user: User, resource: Resource) -> Resource:
+    """Update an existing resource."""
+    resource_id = resource.resource_id
+    authorised = authorised_for(
+        conn, user, ("group:resource:edit-resource",), (resource_id,))
+    if authorised[resource_id]:
+        with db.cursor(conn) as cursor:
+            cursor.execute(
+                "UPDATE resources SET "
+                "resource_name=:resource_name, "
+                "public=:public "
+                "WHERE group_id=:group_id "
+                "AND resource_id=:resource_id",
+                {
+                    "resource_name": resource.resource_name,
+                    "public": 1 if resource.public else 0,
+                    "group_id": str(resource.group.group_id),
+                    "resource_id": str(resource.resource_id)
+                })
+            return resource
+
+    raise AuthorisationError(
+        "You do not have the appropriate privileges to edit this resource.")
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
new file mode 100644
index 0000000..3b2bbeb
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -0,0 +1,272 @@
+"""The views/routes for the resources package"""
+import uuid
+import json
+import sqlite3
+from functools import reduce
+
+from flask import request, jsonify, Response, Blueprint, current_app as app
+
+from gn3.auth.db_utils import with_db_connection
+
+from .checks import authorised_for
+from .models import (
+    Resource, save_resource, resource_data, resource_by_id, resource_categories,
+    assign_resource_user, link_data_to_resource, unassign_resource_user,
+    resource_category_by_id, unlink_data_from_resource,
+    create_resource as _create_resource)
+
+from ..roles import Role
+from ..errors import InvalidData, InconsistencyError, AuthorisationError
+from ..groups.models import Group, GroupRole, group_role_by_id
+
+from ... import db
+from ...dictify import dictify
+from ...authentication.oauth2.resource_server import require_oauth
+from ...authentication.users import User, user_by_id, user_by_email
+
+resources = Blueprint("resources", __name__)
+
+@resources.route("/categories", methods=["GET"])
+@require_oauth("profile group resource")
+def list_resource_categories() -> Response:
+    """Retrieve all resource categories"""
+    db_uri = app.config["AUTH_DB"]
+    with db.connection(db_uri) as conn:
+        return jsonify(tuple(
+            dictify(category) for category in resource_categories(conn)))
+
+@resources.route("/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_resource() -> Response:
+    """Create a new resource"""
+    with require_oauth.acquire("profile group resource") as the_token:
+        form = request.form
+        resource_name = form.get("resource_name")
+        resource_category_id = uuid.UUID(form.get("resource_category"))
+        db_uri = app.config["AUTH_DB"]
+        with db.connection(db_uri) as conn:
+            try:
+                resource = _create_resource(
+                    conn,
+                    resource_name,
+                    resource_category_by_id(conn, resource_category_id),
+                    the_token.user,
+                    (form.get("public") == "on"))
+                return jsonify(dictify(resource))
+            except sqlite3.IntegrityError as sql3ie:
+                if sql3ie.args[0] == ("UNIQUE constraint failed: "
+                                      "resources.resource_name"):
+                    raise InconsistencyError(
+                        "You cannot have duplicate resource names.") from sql3ie
+                app.logger.debug(
+                    f"{type(sql3ie)=}: {sql3ie=}")
+                raise
+
+@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)))
+
+def __safe_get_requests_page__(key: str = "page") -> int:
+    """Get the results page if it exists or default to the first page."""
+    try:
+        return abs(int(request.args.get(key, "1"), base=10))
+    except ValueError as _valerr:
+        return 1
+
+def __safe_get_requests_count__(key: str = "count_per_page") -> int:
+    """Get the results page if it exists or default to the first page."""
+    try:
+        count = request.args.get(key, "0")
+        if count != 0:
+            return abs(int(count, base=10))
+        return 0
+    except ValueError as _valerr:
+        return 0
+
+@resources.route("/view/<uuid:resource_id>/data")
+@require_oauth("profile group resource")
+def view_resource_data(resource_id: uuid.UUID) -> Response:
+    """Retrieve a particular resource's data."""
+    with require_oauth.acquire("profile group resource") as the_token:
+        db_uri = app.config["AUTH_DB"]
+        count_per_page = __safe_get_requests_count__("count_per_page")
+        offset = (__safe_get_requests_page__("page") - 1)
+        with db.connection(db_uri) as conn:
+            resource = resource_by_id(conn, the_token.user, resource_id)
+            return jsonify(resource_data(
+                conn,
+                resource,
+                ((offset * count_per_page) if bool(count_per_page) else offset),
+                count_per_page))
+
+@resources.route("/data/link", methods=["POST"])
+@require_oauth("profile group resource")
+def link_data():
+    """Link group data to a specific resource."""
+    try:
+        form = request.form
+        assert "resource_id" in form, "Resource ID not provided."
+        assert "data_link_id" in form, "Data Link ID not provided."
+        assert "dataset_type" in form, "Dataset type not specified"
+        assert form["dataset_type"].lower() in (
+            "mrna", "genotype", "phenotype"), "Invalid dataset type provided."
+
+        with require_oauth.acquire("profile group resource") as the_token:
+            def __link__(conn: db.DbConnection):
+                return link_data_to_resource(
+                    conn, the_token.user, uuid.UUID(form["resource_id"]),
+                    form["dataset_type"], uuid.UUID(form["data_link_id"]))
+
+            return jsonify(with_db_connection(__link__))
+    except AssertionError as aserr:
+        raise InvalidData(aserr.args[0]) from aserr
+
+
+
+@resources.route("/data/unlink", methods=["POST"])
+@require_oauth("profile group resource")
+def unlink_data():
+    """Unlink data bound to a specific resource."""
+    try:
+        form = request.form
+        assert "resource_id" in form, "Resource ID not provided."
+        assert "data_link_id" in form, "Data Link ID not provided."
+
+        with require_oauth.acquire("profile group resource") as the_token:
+            def __unlink__(conn: db.DbConnection):
+                return unlink_data_from_resource(
+                    conn, the_token.user, uuid.UUID(form["resource_id"]),
+                    uuid.UUID(form["data_link_id"]))
+            return jsonify(with_db_connection(__unlink__))
+    except AssertionError as aserr:
+        raise InvalidData(aserr.args[0]) from aserr
+
+@resources.route("<uuid:resource_id>/user/list", methods=["GET"])
+@require_oauth("profile group resource")
+def resource_users(resource_id: uuid.UUID):
+    """Retrieve all users with access to the given resource."""
+    with require_oauth.acquire("profile group resource") as the_token:
+        def __the_users__(conn: db.DbConnection):
+            resource = resource_by_id(conn, the_token.user, resource_id)
+            authorised = authorised_for(
+                conn, the_token.user, ("group:resource:edit-resource",),
+                (resource_id,))
+            if authorised.get(resource_id, False):
+                with db.cursor(conn) as cursor:
+                    def __organise_users_n_roles__(users_n_roles, row):
+                        user_id = uuid.UUID(row["user_id"])
+                        user = users_n_roles.get(user_id, {}).get(
+                            "user", User(user_id, row["email"], row["name"]))
+                        role = GroupRole(
+                            uuid.UUID(row["group_role_id"]),
+                            resource.group,
+                            Role(uuid.UUID(row["role_id"]), row["role_name"],
+                                 bool(int(row["user_editable"])), tuple()))
+                        return {
+                            **users_n_roles,
+                            user_id: {
+                                "user": user,
+                                "user_group": Group(
+                                    uuid.UUID(row["group_id"]), row["group_name"],
+                                    json.loads(row["group_metadata"])),
+                                "roles": users_n_roles.get(
+                                    user_id, {}).get("roles", tuple()) + (role,)
+                            }
+                        }
+                    cursor.execute(
+                        "SELECT g.*, u.*, r.*, gr.group_role_id "
+                        "FROM groups AS g INNER JOIN "
+                        "group_users AS gu ON g.group_id=gu.group_id "
+                        "INNER JOIN users AS u ON gu.user_id=u.user_id "
+                        "INNER JOIN group_user_roles_on_resources AS guror "
+                        "ON u.user_id=guror.user_id INNER JOIN roles AS r "
+                        "ON guror.role_id=r.role_id "
+                        "INNER JOIN group_roles AS gr ON r.role_id=gr.role_id "
+                        "WHERE guror.resource_id=?",
+                        (str(resource_id),))
+                    return reduce(__organise_users_n_roles__, cursor.fetchall(), {})
+            raise AuthorisationError(
+                "You do not have sufficient privileges to view the resource "
+                "users.")
+        results = (
+            {
+                "user": dictify(row["user"]),
+                "user_group": dictify(row["user_group"]),
+                "roles": tuple(dictify(role) for role in row["roles"])
+            } for row in (
+                user_row for user_id, user_row
+                in with_db_connection(__the_users__).items()))
+        return jsonify(tuple(results))
+
+@resources.route("<uuid:resource_id>/user/assign", methods=["POST"])
+@require_oauth("profile group resource role")
+def assign_role_to_user(resource_id: uuid.UUID) -> Response:
+    """Assign a role on the specified resource to a user."""
+    with require_oauth.acquire("profile group resource role") as the_token:
+        try:
+            form = request.form
+            group_role_id = form.get("group_role_id", "")
+            user_email = form.get("user_email", "")
+            assert bool(group_role_id), "The role must be provided."
+            assert bool(user_email), "The user email must be provided."
+
+            def __assign__(conn: db.DbConnection) -> dict:
+                resource = resource_by_id(conn, the_token.user, resource_id)
+                user = user_by_email(conn, user_email)
+                return assign_resource_user(
+                    conn, resource, user,
+                    group_role_by_id(conn, resource.group,
+                                     uuid.UUID(group_role_id)))
+        except AssertionError as aserr:
+            raise AuthorisationError(aserr.args[0]) from aserr
+
+        return jsonify(with_db_connection(__assign__))
+
+@resources.route("<uuid:resource_id>/user/unassign", methods=["POST"])
+@require_oauth("profile group resource role")
+def unassign_role_to_user(resource_id: uuid.UUID) -> Response:
+    """Unassign a role on the specified resource from a user."""
+    with require_oauth.acquire("profile group resource role") as the_token:
+        try:
+            form = request.form
+            group_role_id = form.get("group_role_id", "")
+            user_id = form.get("user_id", "")
+            assert bool(group_role_id), "The role must be provided."
+            assert bool(user_id), "The user id must be provided."
+
+            def __assign__(conn: db.DbConnection) -> dict:
+                resource = resource_by_id(conn, the_token.user, resource_id)
+                return unassign_resource_user(
+                    conn, resource, user_by_id(conn, uuid.UUID(user_id)),
+                    group_role_by_id(conn, resource.group,
+                                     uuid.UUID(group_role_id)))
+        except AssertionError as aserr:
+            raise AuthorisationError(aserr.args[0]) from aserr
+
+        return jsonify(with_db_connection(__assign__))
+
+@resources.route("<uuid:resource_id>/toggle-public", methods=["POST"])
+@require_oauth("profile group resource role")
+def toggle_public(resource_id: uuid.UUID) -> Response:
+    """Make a resource public if it is private, or private if public."""
+    with require_oauth.acquire("profile group resource") as the_token:
+        def __toggle__(conn: db.DbConnection) -> Resource:
+            old_rsc = resource_by_id(conn, the_token.user, resource_id)
+            return save_resource(
+                conn, the_token.user, Resource(
+                    old_rsc.group, old_rsc.resource_id, old_rsc.resource_name,
+                    old_rsc.resource_category, not old_rsc.public,
+                    old_rsc.resource_data))
+
+        resource = with_db_connection(__toggle__)
+        return jsonify({
+            "resource": dictify(resource),
+            "description": (
+                "Made resource public" if resource.public
+                else "Made resource private")})