about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation/resources
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation/resources')
-rw-r--r--gn_auth/auth/authorisation/resources/base.py14
-rw-r--r--gn_auth/auth/authorisation/resources/checks.py161
-rw-r--r--gn_auth/auth/authorisation/resources/common.py48
-rw-r--r--gn_auth/auth/authorisation/resources/genotypes/__init__.py1
-rw-r--r--gn_auth/auth/authorisation/resources/genotypes/models.py (renamed from gn_auth/auth/authorisation/resources/genotype.py)58
-rw-r--r--gn_auth/auth/authorisation/resources/genotypes/views.py78
-rw-r--r--gn_auth/auth/authorisation/resources/groups/data.py12
-rw-r--r--gn_auth/auth/authorisation/resources/groups/models.py250
-rw-r--r--gn_auth/auth/authorisation/resources/groups/views.py269
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/models.py85
-rw-r--r--gn_auth/auth/authorisation/resources/inbredset/views.py127
-rw-r--r--gn_auth/auth/authorisation/resources/models.py197
-rw-r--r--gn_auth/auth/authorisation/resources/mrna.py9
-rw-r--r--gn_auth/auth/authorisation/resources/phenotype.py68
-rw-r--r--gn_auth/auth/authorisation/resources/phenotypes/__init__.py1
-rw-r--r--gn_auth/auth/authorisation/resources/phenotypes/models.py143
-rw-r--r--gn_auth/auth/authorisation/resources/phenotypes/views.py77
-rw-r--r--gn_auth/auth/authorisation/resources/request_utils.py20
-rw-r--r--gn_auth/auth/authorisation/resources/system/models.py32
-rw-r--r--gn_auth/auth/authorisation/resources/views.py230
20 files changed, 1532 insertions, 348 deletions
diff --git a/gn_auth/auth/authorisation/resources/base.py b/gn_auth/auth/authorisation/resources/base.py
index ac93049..333ba0d 100644
--- a/gn_auth/auth/authorisation/resources/base.py
+++ b/gn_auth/auth/authorisation/resources/base.py
@@ -3,6 +3,8 @@ from uuid import UUID
 from dataclasses import dataclass
 from typing import Any, Sequence
 
+import sqlite3
+
 
 @dataclass(frozen=True)
 class ResourceCategory:
@@ -20,3 +22,15 @@ class Resource:
     resource_category: ResourceCategory
     public: bool
     resource_data: Sequence[dict[str, Any]] = tuple()
+
+
+def resource_from_dbrow(row: sqlite3.Row):
+    """Convert an SQLite3 resultset row into a resource."""
+    return Resource(
+        resource_id=UUID(row["resource_id"]),
+        resource_name=row["resource_name"],
+        resource_category=ResourceCategory(
+            UUID(row["resource_category_id"]),
+            row["resource_category_key"],
+            row["resource_category_description"]),
+        public=bool(int(row["public"])))
diff --git a/gn_auth/auth/authorisation/resources/checks.py b/gn_auth/auth/authorisation/resources/checks.py
index d8e3a9f..bc9e4da 100644
--- a/gn_auth/auth/authorisation/resources/checks.py
+++ b/gn_auth/auth/authorisation/resources/checks.py
@@ -1,14 +1,28 @@
 """Handle authorisation checks for resources"""
-from uuid import UUID
+import uuid
+import logging
+import warnings
 from functools import reduce
 from typing import Sequence
 
+import gn_libs.sqlite3 as authdb
+from gn_libs.privileges import check
+
+from .base import Resource
+from .system.models import system_resource
+
 from ...db import sqlite3 as db
 from ...authentication.users import User
 
+from ..privileges.models import db_row_to_privilege
+
+
+logger = logging.getLogger(__name__)
+
+
 def __organise_privileges_by_resource_id__(rows):
     def __organise__(privs, row):
-        resource_id = UUID(row["resource_id"])
+        resource_id = uuid.UUID(row["resource_id"])
         return {
             **privs,
             resource_id: (row["privilege_id"],) + privs.get(
@@ -16,14 +30,18 @@ def __organise_privileges_by_resource_id__(rows):
         }
     return reduce(__organise__, rows, {})
 
+
 def authorised_for(conn: db.DbConnection,
                    user: User,
                    privileges: tuple[str, ...],
-                   resource_ids: Sequence[UUID]) -> dict[UUID, bool]:
+                   resource_ids: Sequence[uuid.UUID]) -> dict[uuid.UUID, bool]:
     """
     Check whether `user` is authorised to access `resources` according to given
     `privileges`.
     """
+    warnings.warn(DeprecationWarning(
+        f"The function `{__name__}.authorised_for` is deprecated. Please use "
+        f"`{__name__}.authorised_for_spec`"))
     with db.cursor(conn) as cursor:
         cursor.execute(
             ("SELECT ur.*, rp.privilege_id FROM "
@@ -45,3 +63,140 @@ def authorised_for(conn: db.DbConnection,
             resource_id: resource_id in authorised
             for resource_id in resource_ids
         }
+
+
+def authorised_for2(
+        conn: db.DbConnection,
+        user: User,
+        resource: Resource,
+        privileges: tuple[str, ...]
+) -> bool:
+    """
+    Check that `user` has **ALL** the specified privileges for the resource.
+    """
+    warnings.warn(DeprecationWarning(
+        f"The function `{__name__}.authorised_for2` is deprecated. Please use "
+        f"`{__name__}.authorised_for_spec`"))
+    with db.cursor(conn) as cursor:
+        _query = (
+            "SELECT resources.resource_id, user_roles.user_id, roles.role_id, "
+            "privileges.* "
+            "FROM resources INNER JOIN user_roles "
+            "ON resources.resource_id=user_roles.resource_id "
+            "INNER JOIN roles ON user_roles.role_id=roles.role_id "
+            "INNER JOIN role_privileges ON roles.role_id=role_privileges.role_id "
+            "INNER JOIN privileges "
+            "ON role_privileges.privilege_id=privileges.privilege_id "
+            "WHERE resources.resource_id=? "
+            "AND user_roles.user_id=?")
+        cursor.execute(
+            _query,
+            (str(resource.resource_id), str(user.user_id)))
+        _db_privileges = tuple(
+            db_row_to_privilege(row) for row in cursor.fetchall())
+
+    str_privileges = tuple(privilege.privilege_id for privilege in _db_privileges)
+    return all((requested_privilege in str_privileges)
+               for requested_privilege in privileges)
+
+
+def authorised_for_spec(
+        conn: db.DbConnection,
+        user_id: uuid.UUID,
+        resource_id: uuid.UUID,
+        auth_spec: str
+) -> bool:
+    """
+    Check that a user, identified with `user_id`, has a set of privileges that
+    satisfy the `auth_spec` for the resource identified with `resource_id`.
+    """
+    with db.cursor(conn) as cursor:
+        _query = (
+            "SELECT resources.resource_id, user_roles.user_id, roles.role_id, "
+            "privileges.* "
+            "FROM resources INNER JOIN user_roles "
+            "ON resources.resource_id=user_roles.resource_id "
+            "INNER JOIN roles ON user_roles.role_id=roles.role_id "
+            "INNER JOIN role_privileges ON roles.role_id=role_privileges.role_id "
+            "INNER JOIN privileges "
+            "ON role_privileges.privilege_id=privileges.privilege_id "
+            "WHERE resources.resource_id=? "
+            "AND user_roles.user_id=?")
+        cursor.execute(
+            _query,
+            (str(resource_id), str(user_id)))
+        _privileges = tuple(row["privilege_id"] for row in cursor.fetchall())
+    return check(auth_spec, _privileges)
+
+
+def can_delete(
+        conn: authdb.DbConnection,
+        user_id: uuid.UUID,
+        resource_id: uuid.UUID
+) -> bool:
+    """Check whether user is allowed delete a resource and/or its data."""
+    return (
+        authorised_for_spec(# resource-level delete access
+            conn,
+            user_id,
+            resource_id,
+            "(OR group:resource:delete-resource system:resource:delete)")
+        or
+        authorised_for_spec(# system-wide delete access
+            conn,
+            user_id,
+            system_resource(conn).resource_id,
+            "(AND system:system-wide:data:delete)"))
+
+
+def can_view(
+        conn: authdb.DbConnection,
+        user_id: uuid.UUID,
+        resource_id: uuid.UUID
+) -> bool:
+    """Check whether user is allowed view a resource and/or its data."""
+    with authdb.cursor(conn) as cursor:
+        cursor.execute("SELECT public FROM resources WHERE resource_id=?",
+                       (str(resource_id),))
+        row = cursor.fetchone()
+        is_public = bool(row) and bool(int(row["public"]))
+
+    return (
+        is_public# The resource is public, everyone can view!
+        or
+        authorised_for_spec(
+            # resource-level view access: user has view access to his resource.
+            conn,
+            user_id,
+            resource_id,
+            "(OR group:resource:view-resource system:resource:view)")
+        or
+        authorised_for_spec(
+            # system-wide view access: user can view any/all resource(s).
+            conn,
+            user_id,
+            system_resource(conn).resource_id,
+            "(OR system:system-wide:data:view system:resource:view)"))
+
+
+def can_edit(
+        conn: authdb.DbConnection,
+        user_id: uuid.UUID,
+        resource_id: uuid.UUID
+) -> bool:
+    """Check whether user is allowed edit a resource and/or its data."""
+    return (
+        authorised_for_spec(
+            # resource-level edit access: user has edit access to his resource.
+            conn,
+            user_id,
+            resource_id,
+            "(OR group:resource:edit-resource system:resource:edit)")
+        or
+        authorised_for_spec(
+            # system-wide edit access: user can edit any/all resource(s).
+            conn,
+            user_id,
+            system_resource(conn).resource_id,
+            "(OR system:system-wide:data:edit system:resource:edit)"))
+
diff --git a/gn_auth/auth/authorisation/resources/common.py b/gn_auth/auth/authorisation/resources/common.py
new file mode 100644
index 0000000..fd358f1
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/common.py
@@ -0,0 +1,48 @@
+"""Utilities common to more than one resource."""
+import uuid
+
+from gn_auth.auth.db import sqlite3 as db
+
+def assign_resource_owner_role(
+        cursor: db.DbCursor,
+        resource_id: uuid.UUID,
+        user_id: uuid.UUID
+) -> dict:
+    """Assign `user` the 'Resource Owner' role for `resource`."""
+    cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'")
+    role = cursor.fetchone()
+    params = {
+        "user_id": str(user_id),
+        "role_id": role["role_id"],
+        "resource_id": str(resource_id)
+    }
+    cursor.execute(
+        "INSERT INTO user_roles "
+        "VALUES (:user_id, :role_id, :resource_id) "
+        "ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
+        params)
+    return params
+
+
+def grant_access_to_sysadmins(
+        cursor: db.DbCursor,
+        resource_id: uuid.UUID,
+        system_resource_id: uuid.UUID
+):
+    """Grant sysadmins access to resource identified by `resource_id`."""
+    cursor.execute(
+        "SELECT role_id FROM roles WHERE role_name='system-administrator'")
+    sysadminroleid = cursor.fetchone()[0]
+
+    cursor.execute(# Fetch sysadmin IDs.
+        "SELECT user_roles.user_id FROM roles INNER JOIN user_roles "
+        "ON roles.role_id=user_roles.role_id "
+        "WHERE role_name='system-administrator' AND resource_id=?",
+        (str(system_resource_id),))
+
+    cursor.executemany(
+        "INSERT INTO user_roles(user_id, role_id, resource_id) "
+        "VALUES (?, ?, ?) "
+        "ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
+        tuple((row["user_id"], sysadminroleid, str(resource_id))
+              for row in cursor.fetchall()))
diff --git a/gn_auth/auth/authorisation/resources/genotypes/__init__.py b/gn_auth/auth/authorisation/resources/genotypes/__init__.py
new file mode 100644
index 0000000..f401e28
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/genotypes/__init__.py
@@ -0,0 +1 @@
+"""Initialise a genotypes resources package."""
diff --git a/gn_auth/auth/authorisation/resources/genotype.py b/gn_auth/auth/authorisation/resources/genotypes/models.py
index 206ab61..762ee7c 100644
--- a/gn_auth/auth/authorisation/resources/genotype.py
+++ b/gn_auth/auth/authorisation/resources/genotypes/models.py
@@ -5,9 +5,8 @@ from typing import Optional, Sequence
 import sqlite3
 
 import gn_auth.auth.db.sqlite3 as db
-
-from .base import Resource
-from .data import __attach_data__
+from gn_auth.auth.authorisation.resources.base import Resource
+from gn_auth.auth.authorisation.resources.data import __attach_data__
 
 
 def resource_data(
@@ -28,14 +27,15 @@ def resource_data(
 def link_data_to_resource(
         conn: db.DbConnection,
         resource: Resource,
-        data_link_id: uuid.UUID) -> dict:
-    """Link Genotype data with a resource."""
+        data_link_ids: tuple[uuid.UUID, ...]
+) -> tuple[dict, ...]:
+    """Link Genotype data with a resource using the GUI."""
     with db.cursor(conn) as cursor:
-        params = {
+        params = tuple({
             "resource_id": str(resource.resource_id),
             "data_link_id": str(data_link_id)
-        }
-        cursor.execute(
+        } for data_link_id in data_link_ids)
+        cursor.executemany(
             "INSERT INTO genotype_resources VALUES"
             "(:resource_id, :data_link_id)",
             params)
@@ -67,3 +67,45 @@ def attach_resources_data(
         f"WHERE gr.resource_id IN ({placeholders})",
         tuple(str(resource.resource_id) for resource in resources))
     return __attach_data__(cursor.fetchall(), resources)
+
+
+def insert_and_link_data_to_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        cursor,
+        resource_id: uuid.UUID,
+        group_id: uuid.UUID,
+        species_id: int,
+        population_id: int,
+        dataset_id: int,
+        dataset_name: str,
+        dataset_fullname: str,
+        dataset_shortname: str
+) -> dict:
+    """Link the genotype identifier data to the genotype resource."""
+    params = {
+        "resource_id": str(resource_id),
+        "group_id": str(group_id),
+        "data_link_id": str(uuid.uuid4()),
+        "species_id": species_id,
+        "population_id": population_id,
+        "dataset_id": dataset_id,
+        "dataset_name": dataset_name,
+        "dataset_fullname": dataset_fullname,
+        "dataset_shortname": dataset_shortname
+    }
+    cursor.execute(
+        "INSERT INTO linked_genotype_data "
+        "VALUES ("
+        ":data_link_id,"
+        ":group_id,"
+        ":species_id,"
+        ":population_id,"
+        ":dataset_id,"
+        ":dataset_name,"
+        ":dataset_fullname,"
+        ":dataset_shortname"
+        ")",
+        params)
+    cursor.execute(
+        "INSERT INTO genotype_resources VALUES (:resource_id, :data_link_id)",
+        params)
+    return params
diff --git a/gn_auth/auth/authorisation/resources/genotypes/views.py b/gn_auth/auth/authorisation/resources/genotypes/views.py
new file mode 100644
index 0000000..2beed58
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/genotypes/views.py
@@ -0,0 +1,78 @@
+"""Genotype-resources-specific views."""
+import uuid
+
+from pymonad.either import Left, Right
+from flask import jsonify, Blueprint, current_app as app
+
+from gn_auth.auth.db import sqlite3 as db
+from gn_auth.auth.requests import request_json
+
+from gn_auth.auth.authorisation.resources.base import ResourceCategory
+from gn_auth.auth.authorisation.resources.request_utils import check_form
+from gn_auth.auth.authorisation.resources.groups.models import user_group
+
+from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
+
+from gn_auth.auth.authorisation.resources.models import create_resource
+from gn_auth.auth.authorisation.resources.common import (
+    assign_resource_owner_role)
+
+
+from .models import insert_and_link_data_to_resource
+
+genobp = Blueprint("genotypes", __name__)
+
+@genobp.route("genotypes/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_geno_resource():
+    """Create a new genotype resource."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn,
+          db.cursor(conn) as cursor):
+        cursor.execute("SELECT * FROM resource_categories "
+                       "WHERE resource_category_key='genotype'")
+        row = cursor.fetchone()
+
+        return check_form(
+            request_json(),
+            "species_id",
+            "population_id",
+            "dataset_id",
+            "dataset_name",
+            "dataset_fullname",
+            "dataset_shortname"
+        ).then(
+            lambda form: user_group(conn, _token.user).maybe(
+                Left("No user group found!"),
+                lambda group: Right({"formdata": form, "group": group}))
+        ).then(
+            lambda fdgrp: {
+                **fdgrp,
+                "resource": create_resource(
+                    cursor,
+                    f"Genotype — {fdgrp['formdata']['dataset_fullname']}",
+                    ResourceCategory(uuid.UUID(row["resource_category_id"]),
+                                     row["resource_category_key"],
+                                     row["resource_category_description"]),
+                    _token.user,
+                    fdgrp["group"],
+                    fdgrp["formdata"].get("public", "on") == "on")}
+        ).then(
+            lambda fdgrpres: {
+                **fdgrpres,
+                "owner_role": assign_resource_owner_role(
+                    cursor,
+                    fdgrpres["resource"].resource_id,
+                    _token.user.user_id)}
+        ).then(
+            lambda fdgrpres: insert_and_link_data_to_resource(
+                cursor,
+                fdgrpres["resource"].resource_id,
+                fdgrpres["group"].group_id,
+                fdgrpres["formdata"]["species_id"],
+                fdgrpres["formdata"]["population_id"],
+                fdgrpres["formdata"]["dataset_id"],
+                fdgrpres["formdata"]["dataset_name"],
+                fdgrpres["formdata"]["dataset_fullname"],
+                fdgrpres["formdata"]["dataset_shortname"])
+        ).either(lambda error: (jsonify(error), 400), jsonify)
diff --git a/gn_auth/auth/authorisation/resources/groups/data.py b/gn_auth/auth/authorisation/resources/groups/data.py
index 702955d..ad0dfba 100644
--- a/gn_auth/auth/authorisation/resources/groups/data.py
+++ b/gn_auth/auth/authorisation/resources/groups/data.py
@@ -1,7 +1,7 @@
 """Handles the resource objects' data."""
+from gn_libs import mysqldb as gn3db
 from MySQLdb.cursors import DictCursor
 
-from gn_auth.auth.db import mariadb as gn3db
 from gn_auth.auth.db import sqlite3 as authdb
 
 from gn_auth.auth.errors import NotFoundError
@@ -9,7 +9,7 @@ from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.authorisation.resources.groups import Group
 
 def __fetch_mrna_data_by_ids__(
-        conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[
+        conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[
             dict, ...]:
     """Fetch mRNA Assay data by ID."""
     with conn.cursor(DictCursor) as cursor:
@@ -27,7 +27,7 @@ def __fetch_mrna_data_by_ids__(
         raise NotFoundError("Could not find mRNA Assay data with the given ID.")
 
 def __fetch_geno_data_by_ids__(
-        conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[
+        conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[
             dict, ...]:
     """Fetch genotype data by ID."""
     with conn.cursor(DictCursor) as cursor:
@@ -45,7 +45,7 @@ def __fetch_geno_data_by_ids__(
         raise NotFoundError("Could not find Genotype data with the given ID.")
 
 def __fetch_pheno_data_by_ids__(
-        conn: gn3db.DbConnection, dataset_ids: tuple[str, ...]) -> tuple[
+        conn: gn3db.Connection, dataset_ids: tuple[str, ...]) -> tuple[
             dict, ...]:
     """Fetch phenotype data by ID."""
     with conn.cursor(DictCursor) as cursor:
@@ -67,7 +67,7 @@ def __fetch_pheno_data_by_ids__(
             "Could not find Phenotype/Publish data with the given IDs.")
 
 def __fetch_data_by_id(
-        conn: gn3db.DbConnection, dataset_type: str,
+        conn: gn3db.Connection, dataset_type: str,
         dataset_ids: tuple[str, ...]) -> tuple[dict, ...]:
     """Fetch data from MySQL by IDs."""
     fetch_fns = {
@@ -83,7 +83,7 @@ def __fetch_data_by_id(
                   "group(s)."),
               oauth2_scope="profile group resource")
 def link_data_to_group(
-        authconn: authdb.DbConnection, gn3conn: gn3db.DbConnection,
+        authconn: authdb.DbConnection, gn3conn: gn3db.Connection,
         dataset_type: str, dataset_ids: tuple[str, ...], group: Group) -> tuple[
             dict, ...]:
     """Link the given data to the specified group."""
diff --git a/gn_auth/auth/authorisation/resources/groups/models.py b/gn_auth/auth/authorisation/resources/groups/models.py
index 03a93b6..6a7af4c 100644
--- a/gn_auth/auth/authorisation/resources/groups/models.py
+++ b/gn_auth/auth/authorisation/resources/groups/models.py
@@ -5,16 +5,24 @@ from functools import reduce
 from dataclasses import dataclass
 from typing import Any, Sequence, Iterable, Optional
 
+import sqlite3
 from flask import g
 from pymonad.maybe import Just, Maybe, Nothing
+from pymonad.either import Left, Right, Either
+from pymonad.tools import monad_from_none_or_value
 
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.users import User, user_by_id
 
 from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.authorisation.privileges import Privilege
-from gn_auth.auth.authorisation.resources.base import Resource
 from gn_auth.auth.authorisation.resources.errors import MissingGroupError
+from gn_auth.auth.authorisation.resources.system.models import system_resource
+from gn_auth.auth.authorisation.resources.common import (
+    grant_access_to_sysadmins)
+from gn_auth.auth.authorisation.resources.base import (
+    Resource,
+    resource_from_dbrow)
 from gn_auth.auth.errors import (
     NotFoundError, AuthorisationError, InconsistencyError)
 from gn_auth.auth.authorisation.roles.models import (
@@ -63,6 +71,13 @@ class MembershipError(AuthorisationError):
         super().__init__(f"{type(self).__name__}: {error_description}.")
 
 
+def db_row_to_group(row: sqlite3.Row) -> Group:
+    """Convert a database row into a group."""
+    return Group(UUID(row["group_id"]),
+                 row["group_name"],
+                 json.loads(row["group_metadata"]))
+
+
 def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]:
     """Returns all the groups that a member belongs to"""
     query = (
@@ -72,7 +87,7 @@ def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]:
         "WHERE group_users.user_id=?")
     with db.cursor(conn) as cursor:
         cursor.execute(query, (str(user.user_id),))
-        groups = tuple(Group(row[0], row[1], json.loads(row[2]))
+        groups = tuple(Group(row[0], row[1], json.loads(row[2] or "{}"))
                        for row in cursor.fetchall())
 
     return groups
@@ -110,9 +125,10 @@ def create_group(
             cursor, group_name, (
                 {"group_description": group_description}
                 if group_description else {}))
-        group_resource = {
+        _group_resource_id = uuid4()
+        _group_resource = {
             "group_id": str(new_group.group_id),
-            "resource_id": str(uuid4()),
+            "resource_id": str(_group_resource_id),
             "resource_name": group_name,
             "resource_category_id": str(
                 resource_category_by_key(
@@ -123,18 +139,20 @@ def create_group(
         cursor.execute(
             "INSERT INTO resources VALUES "
             "(:resource_id, :resource_name, :resource_category_id, :public)",
-            group_resource)
+            _group_resource)
         cursor.execute(
             "INSERT INTO group_resources(resource_id, group_id) "
             "VALUES(:resource_id, :group_id)",
-            group_resource)
+            _group_resource)
+        grant_access_to_sysadmins(cursor,
+                                  _group_resource_id,
+                                  system_resource(conn).resource_id)
         add_user_to_group(cursor, new_group, group_leader)
         revoke_user_role_by_name(cursor, group_leader, "group-creator")
-        assign_user_role_by_name(
-            cursor,
-            group_leader,
-            UUID(str(group_resource["resource_id"])),
-            "group-leader")
+        assign_user_role_by_name(cursor,
+                                 group_leader,
+                                 _group_resource_id,
+                                 "group-leader")
         return new_group
 
 
@@ -225,15 +243,56 @@ def is_group_leader(conn: db.DbConnection, user: User, group: Group) -> bool:
         return "group-leader" in role_names
 
 
-def all_groups(conn: db.DbConnection) -> Maybe[Sequence[Group]]:
+def __build_groups_list_query__(
+        base: str,
+        search: Optional[str] = None
+) -> tuple[str, tuple[Optional[str], ...]]:
+    """Build up the query from given search terms."""
+    if search is not None and search.strip() != "":
+        _search = search.strip()
+        return ((f"{base} WHERE groups.group_name LIKE ? "
+                 "OR groups.group_metadata LIKE ?"),
+                (f"%{search}%", f"%{search}%"))
+    return base, tuple()
+
+
+def __limit_results_length__(base: str, start: int = 0, length: int = 0) -> str:
+    """Add the `LIMIT … OFFSET …` clause to query `base`."""
+    if length > 0:
+        return f"{base} LIMIT {length} OFFSET {start}"
+    return base
+
+
+def all_groups(
+        conn: db.DbConnection,
+        search: Optional[str] = None,
+        start: int = 0,
+        length: int = 0
+) -> Maybe[tuple[tuple[Group, ...], int, int]]:
     """Retrieve all existing groups"""
     with db.cursor(conn) as cursor:
-        cursor.execute("SELECT * FROM groups")
+        cursor.execute("SELECT COUNT(*) FROM groups")
+        _groups_total_count = int(cursor.fetchone()["COUNT(*)"])
+
+        _qdets = __build_groups_list_query__(
+            "SELECT COUNT(*) FROM groups", search)
+        cursor.execute(*__build_groups_list_query__(
+            "SELECT COUNT(*) FROM groups", search))
+        _filtered_total_count = int(cursor.fetchone()["COUNT(*)"])
+
+        _query, _params = __build_groups_list_query__(
+            "SELECT * FROM groups", search)
+
+        cursor.execute(__limit_results_length__(_query, start, length),
+                       _params)
         res = cursor.fetchall()
         if res:
-            return Just(tuple(
-                Group(row["group_id"], row["group_name"],
-                      json.loads(row["group_metadata"])) for row in res))
+            return Just((
+                tuple(
+                    Group(row["group_id"], row["group_name"],
+                          json.loads(row["group_metadata"])) for row in res),
+                _groups_total_count,
+                _filtered_total_count))
 
     return Nothing
 
@@ -260,6 +319,56 @@ def add_user_to_group(cursor: db.DbCursor, the_group: Group, user: User):
         ("INSERT INTO group_users VALUES (:group_id, :user_id) "
          "ON CONFLICT (group_id, user_id) DO NOTHING"),
         {"group_id": str(the_group.group_id), "user_id": str(user.user_id)})
+    revoke_user_role_by_name(cursor, user, "group-creator")
+
+
+def resource_from_group(conn: db.DbConnection, the_group: Group) -> Resource:
+    """Get the resource object that wraps the group for auth purposes."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT "
+            "resources.resource_id, resources.resource_name, "
+            "resources.public, resource_categories.* "
+            "FROM group_resources "
+            "INNER JOIN resources "
+            "ON group_resources.resource_id=resources.resource_id "
+            "INNER JOIN resource_categories "
+            "ON resources.resource_category_id=resource_categories.resource_category_id "
+            "WHERE group_resources.group_id=?",
+            (str(the_group.group_id),))
+        results = tuple(resource_from_dbrow(row) for row in cursor.fetchall())
+        match len(results):
+            case 0:
+                raise InconsistencyError("The group lacks a wrapper resource.")
+            case 1:
+                return results[0]
+            case _:
+                raise InconsistencyError(
+                    "The group has more than one wrapper resource.")
+
+
+def remove_user_from_group(
+        conn: db.DbConnection,
+        group: Group,
+        user: User,
+        grp_resource: Resource
+):
+    """Add `user` to `group` as a member."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "DELETE FROM group_users "
+            "WHERE group_id=:group_id AND user_id=:user_id",
+            {"group_id": str(group.group_id), "user_id": str(user.user_id)})
+        cursor.execute(
+            "DELETE FROM user_roles WHERE user_id=? AND resource_id=?",
+            (str(user.user_id), str(grp_resource.resource_id)))
+        assign_user_role_by_name(cursor,
+                                 user,
+                                 grp_resource.resource_id,
+                                 "group-creator")
+        grant_access_to_sysadmins(cursor,
+                                  grp_resource.resource_id,
+                                  system_resource(conn).resource_id)
 
 
 @authorised_p(
@@ -319,8 +428,8 @@ gjr.status='PENDING'",
             return tuple(dict(row)for row in cursor.fetchall())
 
     raise AuthorisationError(
-        "You do not have the appropriate authorisation to access the "
-        "group's join requests.")
+        "You need to be the group's leader in order to access the group's join "
+        "requests.")
 
 
 @authorised_p(("system:group:view-group", "system:group:edit-group"),
@@ -489,3 +598,108 @@ def add_resources_to_group(conn: db.DbConnection,
                 "group_id": str(group.group_id),
                 "resource_id": str(rsc.resource_id)
             } for rsc in resources))
+
+
+def admin_group(conn: db.DbConnection) -> Either:
+    """Return a group where at least one system admin is a member."""
+    query = (
+        "SELECT DISTINCT g.group_id, g.group_name, g.group_metadata "
+        "FROM roles AS r INNER JOIN user_roles AS ur ON r.role_id=ur.role_id "
+        "INNER JOIN group_users AS gu ON ur.user_id=gu.user_id "
+        "INNER JOIN groups AS g ON gu.group_id=g.group_id "
+        "WHERE role_name='system-administrator'")
+    with db.cursor(conn) as cursor:
+        cursor.execute(query)
+        return monad_from_none_or_value(
+            Left("There is no group of which the system admininstrator is a "
+                 "member."),
+            lambda row: Right(Group(
+                UUID(row["group_id"]),
+                row["group_name"],
+                json.loads(row["group_metadata"]))),
+            cursor.fetchone())
+
+
+def group_resource(conn: db.DbConnection, group_id: UUID) -> Resource:
+    """Retrieve the system resource."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT group_resources.group_id, resource_categories.*, "
+            "resources.resource_id, resources.resource_name, resources.public "
+            "FROM group_resources INNER JOIN resources "
+            "ON group_resources.resource_id=resources.resource_id "
+            "INNER JOIN resource_categories "
+            "ON resources.resource_category_id=resource_categories.resource_category_id "
+            "WHERE group_resources.group_id=? "
+            "AND resource_categories.resource_category_key='group'",
+            (str(group_id),))
+        row = cursor.fetchone()
+        if row:
+            return resource_from_dbrow(row)
+
+    raise NotFoundError("Could not find a resource for group with ID "
+                        f"{group_id}")
+
+
+def data_resources(
+        conn: db.DbConnection, group_id: UUID) -> Iterable[Resource]:
+    """Fetch a group's data resources."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT resource_ownership.group_id, resources.resource_id, "
+            "resources.resource_name, resources.public, resource_categories.* "
+            "FROM resource_ownership INNER JOIN resources "
+            "ON resource_ownership.resource_id=resources.resource_id "
+            "INNER JOIN resource_categories "
+            "ON resources.resource_category_id=resource_categories.resource_category_id "
+            "WHERE group_id=?",
+            (str(group_id),))
+        yield from (resource_from_dbrow(row) for row in cursor.fetchall())
+
+
+def group_leaders(conn: db.DbConnection, group_id: UUID) -> Iterable[User]:
+    """Fetch all of a group's group leaders."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT users.* FROM group_users INNER JOIN group_resources "
+            "ON group_users.group_id=group_resources.group_id "
+            "INNER JOIN user_roles "
+            "ON group_resources.resource_id=user_roles.resource_id "
+            "INNER JOIN roles "
+            "ON user_roles.role_id=roles.role_id "
+            "INNER JOIN users "
+            "ON user_roles.user_id=users.user_id "
+            "WHERE group_users.group_id=? "
+            "AND roles.role_name='group-leader'",
+            (str(group_id),))
+        yield from (User.from_sqlite3_row(row) for row in cursor.fetchall())
+
+
+def delete_group(conn: db.DbConnection, group_id: UUID):
+    """
+    Delete the group with the given ID
+
+    Parameters:
+        conn (db.DbConnection): an open connection to an SQLite3 database.
+        group_id (uuid.UUID): The identifier for the group to delete.
+
+    Returns:
+        None: It does not return a value.
+
+    Raises:
+        sqlite3.IntegrityError: if the group has members or linked resources, or
+        both.
+    """
+    rsc = group_resource(conn, group_id)
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM group_join_requests WHERE group_id=?",
+                       (str(group_id),))
+        cursor.execute("DELETE FROM user_roles WHERE resource_id=?",
+                       (str(rsc.resource_id),))
+        cursor.execute(
+            "DELETE FROM group_resources WHERE group_id=? AND resource_id=?",
+            (str(group_id), str(rsc.resource_id)))
+        cursor.execute("DELETE FROM resources WHERE resource_id=?",
+                       (str(rsc.resource_id),))
+        cursor.execute("DELETE FROM groups WHERE group_id=?",
+                       (str(group_id),))
diff --git a/gn_auth/auth/authorisation/resources/groups/views.py b/gn_auth/auth/authorisation/resources/groups/views.py
index ef6bb0d..2aa115a 100644
--- a/gn_auth/auth/authorisation/resources/groups/views.py
+++ b/gn_auth/auth/authorisation/resources/groups/views.py
@@ -3,35 +3,47 @@ The views/routes for the `gn3.auth.authorisation.resources.groups` package.
 """
 import uuid
 import datetime
-import warnings
-from typing import Iterable
 from functools import partial
 from dataclasses import asdict
 
+import sqlite3
 from MySQLdb.cursors import DictCursor
-from flask import request, jsonify, Response, Blueprint, current_app
+from flask import jsonify, Response, Blueprint, current_app
 
+from gn_libs import mysqldb as gn3db
+
+from gn_auth.auth.requests import request_json
 from gn_auth.auth.db import sqlite3 as db
-from gn_auth.auth.db import mariadb as gn3db
 from gn_auth.auth.db.sqlite3 import with_db_connection
 
-from gn_auth.auth.authorisation.roles.models import Role
-from gn_auth.auth.authorisation.roles.models import user_roles
-
-from gn_auth.auth.authorisation.checks import authorised_p
-from gn_auth.auth.authorisation.privileges import Privilege, privileges_by_ids
+from gn_auth.auth.authorisation.privileges import privileges_by_ids
 from gn_auth.auth.errors import InvalidData, NotFoundError, AuthorisationError
 
-from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authentication.users import User, user_by_id
 from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
 
+from gn_auth.auth.authorisation.resources.checks import authorised_for_spec
+from gn_auth.auth.authorisation.resources.groups.models import (resource_from_group,
+                                                                remove_user_from_group)
+
 from .data import link_data_to_group
-from .models import (
-    Group, user_group, all_groups, DUMMY_GROUP, GroupRole, group_by_id,
-    join_requests, group_role_by_id, GroupCreationError,
-    accept_reject_join_request, group_users as _group_users,
-    create_group as _create_group, add_privilege_to_group_role,
-    delete_privilege_from_group_role, create_group_role as _create_group_role)
+from .models import (Group,
+                     GroupRole,
+                     user_group,
+                     all_groups,
+                     DUMMY_GROUP,
+                     group_by_id,
+                     group_leaders,
+                     join_requests,
+                     data_resources,
+                     group_role_by_id,
+                     GroupCreationError,
+                     accept_reject_join_request,
+                     add_privilege_to_group_role,
+                     group_users as _group_users,
+                     create_group as _create_group,
+                     delete_group as _delete_group,
+                     delete_privilege_from_group_role)
 
 groups = Blueprint("groups", __name__)
 
@@ -39,26 +51,48 @@ groups = Blueprint("groups", __name__)
 @require_oauth("profile group")
 def list_groups():
     """Return the list of groups that exist."""
+    _kwargs = request_json()
+    def __add_total_group_count__(groups_info):
+        return {
+            "groups": groups_info[0],
+            "total-groups": groups_info[1],
+            "total-filtered": groups_info[2]
+        }
+
     with db.connection(current_app.config["AUTH_DB"]) as conn:
-        the_groups = all_groups(conn)
+        return jsonify(all_groups(
+            conn,
+            search=_kwargs.get("search"),
+            start=int(_kwargs.get("start", "0")),
+            length=int(_kwargs.get("length", "0"))
+        ).then(
+            __add_total_group_count__
+        ).maybe(
+            {
+                "groups": [],
+                "message": "No groups found!",
+                "total-groups": 0,
+                "total-filtered": 0
+            },
+            lambda _grpdata: _grpdata))
 
-    return jsonify(the_groups.maybe(
-        [], lambda grps: [asdict(grp) for grp in grps]))
 
 @groups.route("/create", methods=["POST"])
 @require_oauth("profile group")
 def create_group():
     """Create a new group."""
     with require_oauth.acquire("profile group") as the_token:
-        group_name=request.form.get("group_name", "").strip()
+        group_name=request_json().get("group_name", "").strip()
         if not bool(group_name):
-            raise GroupCreationError("Could not create the group.")
+            raise GroupCreationError(
+                "Could not create the group. Invalid Group name provided was "
+                f"`{group_name}`")
 
         db_uri = current_app.config["AUTH_DB"]
         with db.connection(db_uri) as conn:
             user = the_token.user
             new_group = _create_group(
-                conn, group_name, user, request.form.get("group_description"))
+                conn, group_name, user, request_json().get("group_description"))
             return jsonify({
                 **asdict(new_group), "group_leader": asdict(user)
             })
@@ -107,7 +141,7 @@ def request_to_join(group_id: uuid.UUID) -> Response:
             }
 
     with require_oauth.acquire("profile group") as the_token:
-        form = request.form
+        form = request_json()
         results = with_db_connection(partial(
             __request__, user=the_token.user, group_id=group_id, message=form.get(
                 "message", "I hereby request that you add me to your group.")))
@@ -126,7 +160,7 @@ def list_join_requests() -> Response:
 def accept_join_requests() -> Response:
     """Accept a join request."""
     with require_oauth.acquire("profile group") as the_token:
-        form = request.form
+        form = request_json()
         request_id = uuid.UUID(form.get("request_id"))
         return jsonify(with_db_connection(partial(
             accept_reject_join_request, request_id=request_id,
@@ -137,7 +171,7 @@ def accept_join_requests() -> Response:
 def reject_join_requests() -> Response:
     """Reject a join request."""
     with require_oauth.acquire("profile group") as the_token:
-        form = request.form
+        form = request_json()
         request_id = uuid.UUID(form.get("request_id"))
         return jsonify(with_db_connection(partial(
             accept_reject_join_request, request_id=request_id,
@@ -171,7 +205,7 @@ def unlinked_genotype_data(
         return tuple(dict(row) for row in cursor.fetchall())
 
 def unlinked_phenotype_data(
-        authconn: db.DbConnection, gn3conn: gn3db.DbConnection,
+        authconn: db.DbConnection, gn3conn: gn3db.Connection,
         group: Group) -> tuple[dict, ...]:
     """
     Retrieve all phenotype data linked to a group but not linked to any
@@ -237,7 +271,7 @@ def unlinked_data(resource_type: str) -> Response:
     if resource_type in ("system", "group"):
         return jsonify(tuple())
 
-    if resource_type not in ("all", "mrna", "genotype", "phenotype"):
+    if resource_type not in ("all", "mrna", "genotype", "phenotype", "inbredset-group"):
         raise AuthorisationError(f"Invalid resource type {resource_type}")
 
     with require_oauth.acquire("profile group resource") as the_token:
@@ -255,7 +289,8 @@ def unlinked_data(resource_type: str) -> Response:
                 "genotype": unlinked_genotype_data,
                 "phenotype": lambda conn, grp: partial(
                     unlinked_phenotype_data, gn3conn=gn3conn)(
-                        authconn=conn, group=grp)
+                        authconn=conn, group=grp),
+                "inbredset-group": lambda authconn, ugroup: [] # Still need to implement this
             }
             return jsonify(tuple(
                 dict(row) for row in unlinked_fns[resource_type](
@@ -268,9 +303,9 @@ def unlinked_data(resource_type: str) -> Response:
 def link_data() -> Response:
     """Link selected data to specified group."""
     with require_oauth.acquire("profile group resource") as _the_token:
-        form = request.form
+        form = request_json()
         group_id = uuid.UUID(form["group_id"])
-        dataset_ids = form.getlist("dataset_ids")
+        dataset_ids = form.get("dataset_ids")
         dataset_type = form.get("dataset_type")
         if dataset_type not in ("mrna", "genotype", "phenotype"):
             raise InvalidData("Unexpected dataset type requested!")
@@ -282,70 +317,6 @@ def link_data() -> Response:
 
         return jsonify(with_db_connection(__link__))
 
-@groups.route("/privileges", methods=["GET"])
-@require_oauth("profile group")
-def group_privileges():
-    """Return a list of all available group roles."""
-    with require_oauth.acquire("profile group role") as the_token:
-        def __list_privileges__(conn: db.DbConnection) -> Iterable[Privilege]:
-            ## TODO: Check that user has appropriate privileges
-            this_user_roles = user_roles(conn, the_token.user)
-            with db.cursor(conn) as cursor:
-                cursor.execute("SELECT * FROM privileges "
-                               "WHERE privilege_id LIKE 'group:%'")
-                group_level_roles = tuple(
-                    Privilege(row["privilege_id"], row["privilege_description"])
-                    for row in cursor.fetchall())
-
-            ## the `user_roles(...)` function changed thus this entire function
-            ## needs to change or be obsoleted -- also remove the ignore below
-            return tuple(
-                privilege for arole in this_user_roles["roles"]
-                for privilege in arole.privileges) + group_level_roles #type: ignore[attr-defined]
-        warnings.warn(
-            (f"The `{__name__}.group_privileges` function is broken and will "
-             "be deleted."),
-            PendingDeprecationWarning)
-        return jsonify(tuple(
-            asdict(priv) for priv in with_db_connection(__list_privileges__)))
-
-
-
-@groups.route("/role/create", methods=["POST"])
-@require_oauth("profile group")
-def create_group_role():
-    """Create a new group role."""
-    with require_oauth.acquire("profile group role") as the_token:
-        ## TODO: Check that user has appropriate privileges
-        @authorised_p(("group:role:create-role",),
-                      "You do not have the privilege to create new roles",
-                      oauth2_scope="profile group role")
-        def __create__(conn: db.DbConnection) -> GroupRole:
-            ## TODO: Check user cannot assign any privilege they don't have.
-            form = request.form
-            role_name = form.get("role_name", "").strip()
-            privileges_ids = form.getlist("privileges[]")
-            if len(role_name) == 0:
-                raise InvalidData("Role name not provided!")
-            if len(privileges_ids) == 0:
-                raise InvalidData(
-                    "At least one privilege needs to be provided.")
-
-            group = user_group(conn, the_token.user).maybe(# type: ignore[misc]
-                    DUMMY_GROUP, lambda grp: grp)
-
-            if group == DUMMY_GROUP:
-                raise AuthorisationError(
-                    "A user without a group cannot create a new role.")
-            privileges = privileges_by_ids(conn, tuple(privileges_ids))
-            if len(privileges_ids) != len(privileges):
-                raise InvalidData(
-                    f"{len(privileges_ids) - len(privileges)} of the selected "
-                    "privileges were not found in the database.")
-
-            return _create_group_role(conn, group, role_name, privileges)
-
-        return jsonify(with_db_connection(__create__))
 
 @groups.route("/role/<uuid:group_role_id>", methods=["GET"])
 @require_oauth("profile group")
@@ -374,7 +345,7 @@ def __add_remove_priv_to_from_role__(conn: db.DbConnection,
         raise AuthorisationError(
             "You need to be a member of a group to edit roles.")
     try:
-        privilege_id = request.form.get("privilege_id", "")
+        privilege_id = request_json().get("privilege_id", "")
         assert bool(privilege_id), "Privilege to add must be provided."
         privileges = privileges_by_ids(conn, (privilege_id,))
         if len(privileges) == 0:
@@ -413,3 +384,111 @@ def delete_priv_from_role(group_role_id: uuid.UUID) -> Response:
                 direction="DELETE", user=the_token.user))),
             "description": "Privilege deleted successfully"
         })
+
+
+@groups.route("/<uuid:group_id>", methods=["GET"])
+@require_oauth("profile group")
+def view_group(group_id: uuid.UUID) -> Response:
+    """View a particular group's details."""
+    # TODO: do authorisation checks here…
+    with (require_oauth.acquire("profile group") as _token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        return jsonify(group_by_id(conn, group_id))
+
+
+@groups.route("/<uuid:group_id>/data-resources", methods=["GET"])
+@require_oauth("profile group")
+def view_group_data_resources(group_id: uuid.UUID) -> Response:
+    """View data resources linked to the group."""
+    # TODO: do authorisation checks here…
+    with (require_oauth.acquire("profile group") as _token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        return jsonify(tuple(data_resources(conn, group_id)))
+
+
+@groups.route("/<uuid:group_id>/leaders", methods=["GET"])
+@require_oauth("profile group")
+def view_group_leaders(group_id: uuid.UUID) -> Response:
+    """View a group's leaders."""
+    # TODO: do authorisation checks here…
+    with (require_oauth.acquire("profile group") as _token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        return jsonify(tuple(group_leaders(conn, group_id)))
+
+
+@groups.route("/<uuid:group_id>/remove-member", methods=["POST"])
+@require_oauth("profile group")
+def remove_group_member(group_id: uuid.UUID):
+    """Remove a user as member of this group."""
+    with (require_oauth.acquire("profile group") as _token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        group = group_by_id(conn, group_id)
+        grp_resource = resource_from_group(conn, group)
+        if not authorised_for_spec(
+                conn,
+                _token.user.user_id,
+                grp_resource.resource_id,
+                "(OR group:user:remove-group-member system:group:remove-group-member)"):
+            raise AuthorisationError(
+                "You do not have appropriate privileges to remove a user from this "
+                "group.")
+
+        form = request_json()
+        if not bool(form.get("user_id")):
+            response = jsonify({
+                "error": "MissingUserId",
+                "error-description": (
+                    "Expected 'user_id' value/parameter was not provided.")
+            })
+            response.status_code = 400
+            return response
+
+        try:
+            user = user_by_id(conn, uuid.UUID(form["user_id"]))
+            remove_user_from_group(conn, group, user, grp_resource)
+            success_msg = (
+                f"User '{user.name} ({user.email})' is no longer a member of "
+                f"group '{group.group_name}'.\n"
+                "They could, however, still have access to resources owned by "
+                "the group.")
+            return jsonify({
+                "description": success_msg,
+                "message": success_msg
+            })
+        except ValueError as _verr:
+            response = jsonify({
+                "error": "InvalidUserId",
+                "error-description": "The 'user_id' provided was invalid"
+            })
+            response.status_code = 400
+            return response
+
+
+@groups.route("/<uuid:group_id>/delete", methods=["DELETE"])
+@require_oauth("profile group")
+def delete_group(group_id: uuid.UUID) -> Response:
+    """Delete group with the specified `group_id`."""
+    with (require_oauth.acquire("profile group") as _token,
+          db.connection(current_app.config["AUTH_DB"]) as conn):
+        group = group_by_id(conn, group_id)
+        grp_resource = resource_from_group(conn, group)
+        if not authorised_for_spec(
+                conn,
+                _token.user.user_id,
+                grp_resource.resource_id,
+                "(AND system:group:delete-group)"):
+            raise AuthorisationError(
+                "You do not have appropriate privileges to delete this group.")
+        try:
+            _delete_group(conn, group.group_id)
+            return Response(status=204)
+        except sqlite3.IntegrityError as _s3ie:
+            response = jsonify({
+                "error": "IntegrityError",
+                "error-description": (
+                    "A group that has members, linked resources, or both, "
+                    "cannot be deleted from the system. Remove any members and "
+                    "unlink any linked resources, and try again.")
+            })
+            response.status_code = 400
+            return response
diff --git a/gn_auth/auth/authorisation/resources/inbredset/models.py b/gn_auth/auth/authorisation/resources/inbredset/models.py
new file mode 100644
index 0000000..2626f3e
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/inbredset/models.py
@@ -0,0 +1,85 @@
+"""Functions to handle the low-level details regarding populations auth."""
+from uuid import UUID, uuid4
+from typing import Sequence, Optional
+
+import sqlite3
+
+import gn_auth.auth.db.sqlite3 as db
+from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authorisation.resources.base import Resource
+
+
+def assign_inbredset_group_owner_role(
+        cursor: sqlite3.Cursor,
+        resource: Resource,
+        user: User
+) -> Resource:
+    """
+    Assign `user` as `InbredSet Group Owner` is resource category is
+    'inbredset-group'.
+    """
+    if resource.resource_category.resource_category_key == "inbredset-group":
+        cursor.execute(
+            "SELECT * FROM roles WHERE role_name='inbredset-group-owner'")
+        role = cursor.fetchone()
+        cursor.execute(
+            "INSERT INTO user_roles "
+            "VALUES(:user_id, :role_id, :resource_id) "
+            "ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
+            {
+                "user_id": str(user.user_id),
+                "role_id": str(role["role_id"]),
+                "resource_id": str(resource.resource_id)
+            })
+
+    return resource
+
+
+def link_data_to_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        cursor: sqlite3.Cursor,
+        resource_id: UUID,
+        species_id: int,
+        population_id: int,
+        population_name: str,
+        population_fullname: str
+) -> dict:
+    """Link a species population to a resource for auth purposes."""
+    params = {
+        "resource_id": str(resource_id),
+        "data_link_id": str(uuid4()),
+        "species_id": species_id,
+        "population_id": population_id,
+        "population_name": population_name,
+        "population_fullname": population_fullname
+    }
+    cursor.execute(
+        "INSERT INTO linked_inbredset_groups "
+        "VALUES("
+        " :data_link_id,"
+        " :species_id,"
+        " :population_id,"
+        " :population_name,"
+        " :population_fullname"
+        ")",
+        params)
+    cursor.execute(
+        "INSERT INTO inbredset_group_resources "
+        "VALUES (:resource_id, :data_link_id)",
+        params)
+    return params
+
+
+def resource_data(
+        cursor: db.DbCursor,
+        resource_id: UUID,
+        offset: int = 0,
+        limit: Optional[int] = None) -> Sequence[sqlite3.Row]:
+    """Fetch data linked to a inbred-set resource"""
+    cursor.execute(
+        ("SELECT * FROM inbredset_group_resources AS igr "
+         "INNER JOIN linked_inbredset_groups AS lig "
+         "ON igr.data_link_id=lig.data_link_id "
+         "WHERE igr.resource_id=?") + (
+             f" LIMIT {limit} OFFSET {offset}" if bool(limit) else ""),
+        (str(resource_id),))
+    return cursor.fetchall()
diff --git a/gn_auth/auth/authorisation/resources/inbredset/views.py b/gn_auth/auth/authorisation/resources/inbredset/views.py
index 444c442..9603b5b 100644
--- a/gn_auth/auth/authorisation/resources/inbredset/views.py
+++ b/gn_auth/auth/authorisation/resources/inbredset/views.py
@@ -1,12 +1,56 @@
 """Views for InbredSet resources."""
-from flask import jsonify, Response, Blueprint
+import uuid
+
+from pymonad.either import Left, Right, Either
+from flask import jsonify, Response, Blueprint, current_app as app
+
 
 from gn_auth.auth.db import sqlite3 as db
-from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_auth.auth.errors import NotFoundError
+from gn_auth.auth.requests import request_json
+from gn_auth.auth.authentication.users import User
+from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
+from gn_auth.auth.authorisation.resources.base import Resource, ResourceCategory
+from gn_auth.auth.authorisation.resources.groups.models import (Group,
+                                                                user_group,
+                                                                admin_group)
+from gn_auth.auth.authorisation.resources.models import (
+    create_resource as _create_resource)
+
+from .models import (link_data_to_resource,
+                     assign_inbredset_group_owner_role)
+
+popbp = Blueprint("populations", __name__)
+
 
-iset = Blueprint("inbredset", __name__)
+def create_resource(
+        cursor: db.DbCursor,
+        resource_name: str,
+        user: User,
+        group: Group,
+        public: bool
+) -> Resource:
+    """Convenience function to create a resource of type 'inbredset-group'."""
+    cursor.execute("SELECT * FROM resource_categories "
+                   "WHERE resource_category_key='inbredset-group'")
+    category = cursor.fetchone()
+    if category:
+        return _create_resource(cursor,
+                                resource_name,
+                                ResourceCategory(
+                                    resource_category_id=uuid.UUID(
+                                        category["resource_category_id"]),
+                                    resource_category_key="inbredset-group",
+                                    resource_category_description=category[
+                                        "resource_category_description"]),
+                                user,
+                                group,
+                                public)
+    raise NotFoundError("Could not find a 'inbredset-group' resource category.")
 
-@iset.route("/resource-id/<int:speciesid>/<int:inbredsetid>")
+
+@popbp.route("/populations/resource-id/<int:speciesid>/<int:inbredsetid>",
+            methods=["GET"])
 def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
     """Retrieve the resource ID for resource attached to the inbredset."""
     def __res_by_iset_id__(conn):
@@ -20,7 +64,7 @@ def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
                 (speciesid, inbredsetid))
             return cursor.fetchone()
 
-    res = with_db_connection(__res_by_iset_id__)
+    res = db.with_db_connection(__res_by_iset_id__)
     if res:
         resp = jsonify({"status": "success", "resource-id": res["resource_id"]})
     else:
@@ -34,3 +78,76 @@ def resource_id_by_inbredset_id(speciesid: int, inbredsetid: int) -> Response:
         resp.status_code = 404
 
     return resp
+
+
+@popbp.route("/populations/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_population_resource():
+    """Create a resource of type 'inbredset-group'."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn,
+          db.cursor(conn) as cursor):
+
+        def __check_form__(form, usergroup) -> Either:
+            """Check form for errors."""
+            errors: tuple[str, ...] = tuple()
+
+            species_id = form.get("species_id")
+            if not bool(species_id):
+                errors = errors + ("Missing `species_id` value.",)
+
+            population_id = form.get("population_id")
+            if not bool(population_id):
+                errors = errors + ("Missing `population_id` value.",)
+
+            population_name = form.get("population_name")
+            if not bool(population_name):
+                errors = errors + ("Missing `population_name` value.",)
+
+            population_fullname = form.get("population_fullname")
+            if not bool(population_fullname):
+                errors = errors + ("Missing `population_fullname` value.",)
+
+            if bool(errors):
+                error_messages = "\n\t - ".join(errors)
+                return Left({
+                    "error": "Invalid Request Data!",
+                    "error_description": error_messages
+                })
+
+            return Right({"formdata": form, "group": usergroup})
+
+        def __default_group_if_none__(group) -> Either:
+            if group.is_nothing():
+                return admin_group(conn)
+            return Right(group.value)
+
+        return __default_group_if_none__(
+            user_group(conn, _token.user)
+        ).then(
+            lambda group: __check_form__(request_json(), group)
+        ).then(
+            lambda formdata: {
+                **formdata,
+                "resource": create_resource(
+                    cursor,
+                    f"Population — {formdata['formdata']['population_name']}",
+                    _token.user,
+                    formdata["group"],
+                    formdata["formdata"].get("public", "on") == "on")}
+        ).then(
+            lambda resource: {
+                **resource,
+                "resource": assign_inbredset_group_owner_role(
+                    cursor, resource["resource"], _token.user)}
+        ).then(
+            lambda resource: link_data_to_resource(
+                cursor,
+                resource["resource"].resource_id,
+                resource["formdata"]["species_id"],
+                resource["formdata"]["population_id"],
+                resource["formdata"]["population_name"],
+                resource["formdata"]["population_fullname"])
+        ).either(
+            lambda error: (jsonify(error), 400),
+            jsonify)
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py
index 95a7f1c..a4df363 100644
--- a/gn_auth/auth/authorisation/resources/models.py
+++ b/gn_auth/auth/authorisation/resources/models.py
@@ -2,9 +2,10 @@
 from dataclasses import asdict
 from uuid import UUID, uuid4
 from functools import reduce, partial
-from typing import Dict, Sequence, Optional
+from typing import Dict, Union, Sequence, Optional
+
+from gn_libs import sqlite3 as db
 
-from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.users import User
 from gn_auth.auth.db.sqlite3 import with_db_connection
 
@@ -13,69 +14,42 @@ from gn_auth.auth.authorisation.privileges import Privilege
 from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.errors import NotFoundError, AuthorisationError
 
-from .checks import authorised_for
-from .base import Resource, ResourceCategory
-from .groups.models import Group, GroupRole, user_group, is_group_leader
+from .system.models import system_resource
+from .checks import authorised_for, authorised_for_spec
+from .base import Resource, ResourceCategory, resource_from_dbrow
+from .common import assign_resource_owner_role, grant_access_to_sysadmins
+from .groups.models import Group, is_group_leader
+from .inbredset.models import resource_data as inbredset_resource_data
 from .mrna import (
     resource_data as mrna_resource_data,
     attach_resources_data as mrna_attach_resources_data,
     link_data_to_resource as mrna_link_data_to_resource,
     unlink_data_from_resource as mrna_unlink_data_from_resource)
-from .genotype import (
+from .genotypes.models import (
     resource_data as genotype_resource_data,
     attach_resources_data as genotype_attach_resources_data,
     link_data_to_resource as genotype_link_data_to_resource,
     unlink_data_from_resource as genotype_unlink_data_from_resource)
-from .phenotype import (
+from .phenotypes.models import (
     resource_data as phenotype_resource_data,
     attach_resources_data as phenotype_attach_resources_data,
     link_data_to_resource as phenotype_link_data_to_resource,
     unlink_data_from_resource as phenotype_unlink_data_from_resource)
 
-from .errors import MissingGroupError
-
-def __assign_resource_owner_role__(cursor, resource, user):
-    """Assign `user` the 'Resource Owner' role for `resource`."""
-    cursor.execute(
-        "SELECT rr.* FROM resource_roles AS rr INNER JOIN roles AS r "
-        "ON rr.role_id=r.role_id WHERE r.role_name='resource-owner' "
-        "AND rr.resource_id=?",
-        (str(resource.resource_id),))
-    role = cursor.fetchone()
-    if not role:
-        cursor.execute("SELECT * FROM roles WHERE role_name='resource-owner'")
-        role = cursor.fetchone()
-        cursor.execute(
-            "INSERT INTO resource_roles(resource_id, role_created_by, role_id) "
-            "VALUES (:resource_id, :user_id, :role_id)",
-            {"resource_id": str(resource.resource_id),
-             "user_id": str(user.user_id),
-             "role_id": role["role_id"]})
-
-    cursor.execute(
-        "INSERT INTO user_roles "
-        "VALUES (:user_id, :role_id, :resource_id) "
-        "ON CONFLICT (user_id, role_id, resource_id) DO NOTHING",
-        {
-            "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:
+def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        conn: Union[db.DbConnection, db.DbCursor],
+        resource_name: str,
+        resource_category: ResourceCategory,
+        user: User,
+        group: Group,
+        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(# Not all resources require an owner group
-                "User with no group cannot create a resource.")
+    def __create_resource__(cursor: db.DbCursor) -> Resource:
         resource = Resource(uuid4(), resource_name, resource_category, public)
         cursor.execute(
             "INSERT INTO resources VALUES (?, ?, ?, ?)",
@@ -83,12 +57,46 @@ def create_resource(
              resource_name,
              str(resource.resource_category.resource_category_id),
              1 if resource.public else 0))
+        # TODO: @fredmanglis,@rookie101
+        # 1. Move the actions below into a (the?) hooks system
+        # 2. Do more checks: A resource can have varying hooks depending on type
+        #    e.g. if mRNA, pheno or geno resource, assign:
+        #           - "resource-owner"
+        #         if inbredset-group, assign:
+        #           - "resource-owner",
+        #           - "inbredset-group-owner" etc.
+        #         if resource is of type "group", assign:
+        #           - group-leader
         cursor.execute("INSERT INTO resource_ownership (group_id, resource_id) "
                        "VALUES (?, ?)",
                        (str(group.group_id), str(resource.resource_id)))
-        __assign_resource_owner_role__(cursor, resource, user)
+        assign_resource_owner_role(cursor, resource.resource_id, user.user_id)
+        grant_access_to_sysadmins(
+            cursor, resource.resource_id, system_resource(conn).resource_id)
+
+        return resource
+
+    if hasattr(conn, "cursor"): # This is a connection: get its cursor.
+        with db.cursor(conn) as cursor:
+            return __create_resource__(cursor)
+    else:
+        return __create_resource__(conn)
+
+
+def delete_resource(conn: db.DbConnection, resource_id: UUID):
+    """Delete a resource."""
+    with db.cursor(conn) as cursor:
+        cursor.execute("DELETE FROM user_roles WHERE resource_id=?",
+                       (str(resource_id),))
+        cursor.execute("DELETE FROM resource_roles WHERE resource_id=?",
+                       (str(resource_id),))
+        cursor.execute("DELETE FROM group_resources WHERE resource_id=?",
+                       (str(resource_id),))
+        cursor.execute("DELETE FROM resource_ownership WHERE resource_id=?",
+                       (str(resource_id),))
+        cursor.execute("DELETE FROM resources WHERE resource_id=?",
+                       (str(resource_id),))
 
-    return resource
 
 def resource_category_by_id(
         conn: db.DbConnection, category_id: UUID) -> ResourceCategory:
@@ -149,32 +157,21 @@ def group_leader_resources(
 
 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 DISTINCT(r.resource_id), r.resource_name,  "
+             "r.resource_category_id, r.public, rc.resource_category_key, "
+             "rc.resource_category_description "
+             "FROM user_roles AS ur "
+             "INNER JOIN resources AS r ON ur.resource_id=r.resource_id "
+             "INNER JOIN resource_categories AS rc "
+             "ON r.resource_category_id=rc.resource_category_id "
+             "WHERE ur.user_id=?"),
+            (str(user.user_id),))
+        rows = cursor.fetchall() or []
+
+    return tuple(resource_from_dbrow(row) for row in rows)
 
-            cursor.execute(
-                ("SELECT resources.* FROM user_roles LEFT JOIN resources "
-                 "ON user_roles.resource_id=resources.resource_id "
-                 "WHERE user_roles.user_id=?"),
-                (str(user.user_id),))
-            rows = cursor.fetchall()
-            private_res = tuple(
-                Resource(UUID(row[0]), row[1], categories[UUID(row[2])],
-                         bool(row[3]))
-                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, ...]:
     """
@@ -188,7 +185,8 @@ def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None)
         "genotype-metadata": lambda *args: tuple(),
         "mrna-metadata": lambda *args: tuple(),
         "system": lambda *args: tuple(),
-        "group": lambda *args: tuple()
+        "group": lambda *args: tuple(),
+        "inbredset-group": inbredset_resource_data,
     }
     with db.cursor(conn) as cursor:
         return tuple(
@@ -216,9 +214,11 @@ def attach_resource_data(cursor: db.DbCursor, resource: Resource) -> Resource:
 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]:
+    if not authorised_for_spec(
+            conn,
+            user.user_id,
+            resource_id,
+            "(OR group:resource:view-resource system:resource:view)"):
         raise AuthorisationError(
             "You are not authorised to access resource with id "
             f"'{resource_id}'.")
@@ -236,8 +236,12 @@ def resource_by_id(
     raise NotFoundError(f"Could not find a resource with id '{resource_id}'")
 
 def link_data_to_resource(
-        conn: db.DbConnection, user: User, resource_id: UUID, dataset_type: str,
-        data_link_id: UUID) -> dict:
+        conn: db.DbConnection,
+        user: User,
+        resource_id: UUID,
+        dataset_type: str,
+        data_link_ids: tuple[UUID, ...]
+) -> tuple[dict, ...]:
     """Link data to resource."""
     if not authorised_for(
             conn, user, ("group:resource:edit-resource",),
@@ -252,7 +256,7 @@ def link_data_to_resource(
         "mrna": mrna_link_data_to_resource,
         "genotype": genotype_link_data_to_resource,
         "phenotype": phenotype_link_data_to_resource,
-    }[dataset_type.lower()](conn, resource, data_link_id)
+    }[dataset_type.lower()](conn, resource, data_link_ids)
 
 def unlink_data_from_resource(
         conn: db.DbConnection, user: User, resource_id: UUID, data_link_id: UUID):
@@ -305,13 +309,13 @@ def attach_resources_data(
              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:
+        conn: db.DbConnection,
+        resource: Resource,
+        user: User,
+        role: Role
+) -> dict:
     """Assign `role` to `user` for the specific `resource`."""
     with db.cursor(conn) as cursor:
         cursor.execute(
@@ -319,39 +323,36 @@ def assign_resource_user(
             "VALUES (?, ?, ?) "
             "ON CONFLICT (user_id, role_id, resource_id) "
             "DO NOTHING",
-            (str(user.user_id), str(role.role.role_id),
-             str(resource.resource_id)))
+            (str(user.user_id), str(role.role_id), str(resource.resource_id)))
         return {
             "resource": asdict(resource),
             "user": asdict(user),
             "role": asdict(role),
             "description": (
                 f"The user '{user.name}'({user.email}) was assigned the "
-                f"'{role.role.role_name}' role on resource with ID "
+                f"'{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:
+        conn: db.DbConnection,
+        resource: Resource,
+        user: User,
+        role: Role
+) -> dict:
     """Assign `role` to `user` for the specific `resource`."""
     with db.cursor(conn) as cursor:
         cursor.execute(
             "DELETE FROM user_roles "
             "WHERE user_id=? AND role_id=? AND resource_id=?",
-            (str(user.user_id),
-             str(role.role.role_id),
-             str(resource.resource_id)))
+            (str(user.user_id), str(role.role_id), str(resource.resource_id)))
         return {
             "resource": asdict(resource),
             "user": asdict(user),
             "role": asdict(role),
             "description": (
                 f"The user '{user.name}'({user.email}) had the "
-                f"'{role.role.role_name}' role on resource with ID "
+                f"'{role.role_name}' role on resource with ID "
                 f"'{resource.resource_id}' taken away.")}
 
 def save_resource(
diff --git a/gn_auth/auth/authorisation/resources/mrna.py b/gn_auth/auth/authorisation/resources/mrna.py
index 7fce227..66f8824 100644
--- a/gn_auth/auth/authorisation/resources/mrna.py
+++ b/gn_auth/auth/authorisation/resources/mrna.py
@@ -26,14 +26,15 @@ def resource_data(cursor: db.DbCursor,
 def link_data_to_resource(
         conn: db.DbConnection,
         resource: Resource,
-        data_link_id: uuid.UUID) -> dict:
+        data_link_ids: tuple[uuid.UUID, ...]
+) -> tuple[dict, ...]:
     """Link mRNA Assay data with a resource."""
     with db.cursor(conn) as cursor:
-        params = {
+        params = tuple({
             "resource_id": str(resource.resource_id),
             "data_link_id": str(data_link_id)
-        }
-        cursor.execute(
+        } for data_link_id in data_link_ids)
+        cursor.executemany(
             "INSERT INTO mrna_resources VALUES"
             "(:resource_id, :data_link_id)",
             params)
diff --git a/gn_auth/auth/authorisation/resources/phenotype.py b/gn_auth/auth/authorisation/resources/phenotype.py
deleted file mode 100644
index 7005db3..0000000
--- a/gn_auth/auth/authorisation/resources/phenotype.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""Phenotype data resources functions and utilities."""
-import uuid
-from typing import Optional, Sequence
-
-import sqlite3
-
-import gn_auth.auth.db.sqlite3 as db
-
-from .base import Resource
-from .data import __attach_data__
-
-def resource_data(
-        cursor: db.DbCursor,
-        resource_id: uuid.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 link_data_to_resource(
-        conn: db.DbConnection,
-        resource: Resource,
-        data_link_id: uuid.UUID) -> dict:
-    """Link Phenotype data with a resource."""
-    with db.cursor(conn) as cursor:
-        params = {
-            "resource_id": str(resource.resource_id),
-            "data_link_id": str(data_link_id)
-        }
-        cursor.execute(
-            "INSERT INTO phenotype_resources VALUES"
-            "(:resource_id, :data_link_id)",
-            params)
-        return params
-
-def unlink_data_from_resource(
-        conn: db.DbConnection,
-        resource: Resource,
-        data_link_id: uuid.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 attach_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)
diff --git a/gn_auth/auth/authorisation/resources/phenotypes/__init__.py b/gn_auth/auth/authorisation/resources/phenotypes/__init__.py
new file mode 100644
index 0000000..0d4dbfa
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/phenotypes/__init__.py
@@ -0,0 +1 @@
+"""The phenotypes package."""
diff --git a/gn_auth/auth/authorisation/resources/phenotypes/models.py b/gn_auth/auth/authorisation/resources/phenotypes/models.py
new file mode 100644
index 0000000..0ef91ab
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/phenotypes/models.py
@@ -0,0 +1,143 @@
+"""Phenotype data resources functions and utilities."""
+import uuid
+from functools import reduce
+from typing import Optional, Sequence
+
+import sqlite3
+from pymonad.maybe import Just, Maybe, Nothing
+from pymonad.tools import monad_from_none_or_value
+
+import gn_auth.auth.db.sqlite3 as db
+from gn_auth.auth.authorisation.resources.data import __attach_data__
+from gn_auth.auth.authorisation.resources.base import Resource, resource_from_dbrow
+
+def resource_data(
+        cursor: db.DbCursor,
+        resource_id: uuid.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 link_data_to_resource(
+        conn: db.DbConnection,
+        resource: Resource,
+        data_link_ids: tuple[uuid.UUID, ...]
+) -> tuple[dict, ...]:
+    """Link Phenotype data with a resource."""
+    with db.cursor(conn) as cursor:
+        params = tuple({
+            "resource_id": str(resource.resource_id),
+            "data_link_id": str(data_link_id)
+        } for data_link_id in data_link_ids)
+        cursor.executemany(
+            "INSERT INTO phenotype_resources VALUES"
+            "(:resource_id, :data_link_id)",
+            params)
+        return params
+
+def unlink_data_from_resource(
+        conn: db.DbConnection,
+        resource: Resource,
+        data_link_id: uuid.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 attach_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 individual_linked_resource(
+        conn: db.DbConnection,
+        species_id: int,
+        population_id: int,
+        dataset_id: int,
+        xref_id: str) -> Maybe:
+    """Given the data details, return the linked resource, if one is defined."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT "
+            "rsc.*, rc.*, lpd.SpeciesId AS species_id, "
+            "lpd.InbredSetId AS population_id, lpd.PublishXRefId AS xref_id, "
+            "lpd.dataset_name, lpd.dataset_fullname, lpd.dataset_shortname "
+            "FROM linked_phenotype_data AS lpd "
+            "INNER JOIN phenotype_resources AS pr "
+            "ON lpd.data_link_id=pr.data_link_id "
+            "INNER JOIN resources AS rsc ON pr.resource_id=rsc.resource_id "
+            "INNER JOIN resource_categories AS rc "
+            "ON rsc.resource_category_id=rc.resource_category_id "
+            "WHERE "
+            "(lpd.SpeciesId, lpd.InbredSetId, lpd.PublishFreezeId, lpd.PublishXRefId) = "
+            "(?, ?, ?, ?)",
+            (species_id, population_id, dataset_id, xref_id))
+        return monad_from_none_or_value(
+            Nothing, Just, cursor.fetchone()).then(resource_from_dbrow)
+
+
+def all_linked_resources(
+        conn: db.DbConnection,
+        species_id: int,
+        population_id: int,
+        dataset_id: int) -> Maybe:
+    """Given the data details, return the linked resource, if one is defined."""
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            "SELECT rsc.*, rc.resource_category_key, "
+            "rc.resource_category_description, lpd.SpeciesId AS species_id, "
+            "lpd.InbredSetId AS population_id, lpd.PublishXRefId AS xref_id, "
+            "lpd.dataset_name, lpd.dataset_fullname, lpd.dataset_shortname "
+            "FROM linked_phenotype_data AS lpd "
+            "INNER JOIN phenotype_resources AS pr "
+            "ON lpd.data_link_id=pr.data_link_id INNER JOIN resources AS rsc "
+            "ON pr.resource_id=rsc.resource_id "
+            "INNER JOIN resource_categories AS rc "
+            "ON rsc.resource_category_id=rc.resource_category_id "
+            "WHERE "
+            "(lpd.SpeciesId, lpd.InbredSetId, lpd.PublishFreezeId) = (?, ?, ?)",
+            (species_id, population_id, dataset_id))
+
+        _rscdatakeys = (
+            "species_id", "population_id", "xref_id", "dataset_name",
+            "dataset_fullname", "dataset_shortname")
+        def __organise__(resources, row):
+            _rscid = uuid.UUID(row["resource_id"])
+            _resource = resources.get(_rscid, resource_from_dbrow(row))
+            return {
+                **resources,
+                _rscid: Resource(
+                    _resource.resource_id,
+                    _resource.resource_name,
+                    _resource.resource_category,
+                    _resource.public,
+                    _resource.resource_data + (
+                             {key: row[key] for key in _rscdatakeys},))
+            }
+        results: dict[uuid.UUID, Resource] = reduce(
+            __organise__, cursor.fetchall(), {})
+        if len(results) == 0:
+            return Nothing
+        return Just(tuple(results.values()))
diff --git a/gn_auth/auth/authorisation/resources/phenotypes/views.py b/gn_auth/auth/authorisation/resources/phenotypes/views.py
new file mode 100644
index 0000000..c0a5e81
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/phenotypes/views.py
@@ -0,0 +1,77 @@
+"""Views for the phenotype resources."""
+from pymonad.either import Left, Right
+from flask import jsonify, Blueprint, current_app as app
+
+from gn_auth.auth.db import sqlite3 as db
+from gn_auth.auth.requests import request_json
+from gn_auth.auth.authorisation.resources.request_utils import check_form
+from gn_auth.auth.authorisation.roles.models import user_roles_on_resource
+
+from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
+
+from .models import all_linked_resources, individual_linked_resource
+
+phenobp = Blueprint("phenotypes", __name__)
+
+@phenobp.route("/phenotypes/individual/linked-resource", methods=["POST"])
+@require_oauth("profile group resource")
+def get_individual_linked_resource():
+    """Get the linked resource for a particular phenotype within the dataset.
+
+    Phenotypes are a tad tricky. Each phenotype could technically be a resource
+    on its own, and thus a user could have access to only a subset of phenotypes
+    within the entire dataset."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        return check_form(
+            request_json(),
+            "species_id",
+            "population_id",
+            "dataset_id",
+            "xref_id"
+        ).then(
+            lambda formdata: individual_linked_resource(
+                    conn,
+                    int(formdata["species_id"]),
+                    int(formdata["population_id"]),
+                    int(formdata["dataset_id"]),
+                    formdata["xref_id"]
+                ).maybe(Left("No linked resource!"),
+                        lambda lrsc: Right({
+                            "formdata": formdata,
+                            "resource": lrsc
+                        }))
+        ).then(
+            lambda fdlrsc: {
+                **fdlrsc,
+                "roles": user_roles_on_resource(
+                    conn, _token.user.user_id, fdlrsc["resource"].resource_id)
+            }
+        ).either(lambda error: (jsonify(error), 400),
+                 lambda res: jsonify({
+                     key: value for key, value in res.items()
+                     if key != "formdata"
+                 }))
+
+
+@phenobp.route("/phenotypes/linked-resources", methods=["POST"])
+@require_oauth("profile group resource")
+def get_all_linked_resources():
+    """Get all the linked resources for all phenotypes within a dataset.
+
+    See `get_individual_linked_resource(…)` documentation."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        return check_form(
+            request_json(),
+            "species_id",
+            "population_id",
+            "dataset_id"
+        ).then(
+            lambda formdata: all_linked_resources(
+                conn,
+                int(formdata["species_id"]),
+                int(formdata["population_id"]),
+                int(formdata["dataset_id"])).maybe(
+                    Left("No linked resource!"), Right)
+        ).either(lambda error: (jsonify(error), 400), jsonify)
diff --git a/gn_auth/auth/authorisation/resources/request_utils.py b/gn_auth/auth/authorisation/resources/request_utils.py
new file mode 100644
index 0000000..ade779e
--- /dev/null
+++ b/gn_auth/auth/authorisation/resources/request_utils.py
@@ -0,0 +1,20 @@
+"""Some common utils for requests to the resources endpoints."""
+from functools import reduce
+
+from pymonad.either import Left, Right, Either
+
+def check_form(form, *fields) -> Either:
+    """Check form for errors"""
+    def __check_field__(errors, field):
+        if not bool(form.get(field)):
+            return errors + (f"Missing `{field}` value.",)
+        return errors
+
+    errors: tuple[str, ...] = reduce(__check_field__, fields, tuple())
+    if len(errors) > 0:
+        return Left({
+            "error": "Invalid request data!",
+            "error_description": "\n\t - ".join(errors)
+        })
+
+    return Right(form)
diff --git a/gn_auth/auth/authorisation/resources/system/models.py b/gn_auth/auth/authorisation/resources/system/models.py
index 7c176aa..25089fa 100644
--- a/gn_auth/auth/authorisation/resources/system/models.py
+++ b/gn_auth/auth/authorisation/resources/system/models.py
@@ -1,14 +1,19 @@
 """Base functions and utilities for system resources."""
 from uuid import UUID
 from functools import reduce
-from typing import Sequence
+from typing import Union, Sequence
 
-from gn_auth.auth.db import sqlite3 as db
+from gn_libs import sqlite3 as db
+
+from gn_auth.auth.errors import NotFoundError
 
 from gn_auth.auth.authentication.users import User
 
 from gn_auth.auth.authorisation.roles import Role
 from gn_auth.auth.authorisation.privileges import Privilege
+from gn_auth.auth.authorisation.resources.base import (
+    Resource,
+    resource_from_dbrow)
 
 def __organise_privileges__(acc, row):
     role_id = UUID(row["role_id"])
@@ -24,6 +29,7 @@ def __organise_privileges__(acc, row):
              (Privilege(row["privilege_id"], row["privilege_description"]),)))
     }
 
+
 def user_roles_on_system(conn: db.DbConnection, user: User) -> Sequence[Role]:
     """
     Retrieve all roles assigned to the `user` that act on `system` resources.
@@ -45,3 +51,25 @@ def user_roles_on_system(conn: db.DbConnection, user: User) -> Sequence[Role]:
         return tuple(reduce(
             __organise_privileges__, cursor.fetchall(), {}).values())
     return tuple()
+
+
+def system_resource(conn: Union[db.DbConnection, db.DbCursor]) -> Resource:
+    """Retrieve the system resource."""
+    def __fetch_sys_resource__(cursor: db.DbCursor) -> Resource:
+        cursor.execute(
+            "SELECT resource_categories.*, resources.resource_id, "
+            "resources.resource_name, resources.public "
+            "FROM resource_categories INNER JOIN resources "
+            "ON resource_categories.resource_category_id=resources.resource_category_id "
+            "WHERE resource_categories.resource_category_key='system'")
+        row = cursor.fetchone()
+        if row:
+            return resource_from_dbrow(row)
+
+        raise NotFoundError("Could not find a system resource!")
+
+    if hasattr(conn, "cursor"): # is connection
+        with db.cursor(conn) as cursor:
+            return __fetch_sys_resource__(cursor)
+    else:
+        return __fetch_sys_resource__(conn)
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index 50f0d8e..e4401c5 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -8,36 +8,56 @@ import time
 from dataclasses import asdict
 from functools import reduce
 
-from authlib.integrations.flask_oauth2.errors import _HTTPException
+from werkzeug.exceptions import BadRequest
 from authlib.jose import jwt
+from authlib.integrations.flask_oauth2.errors import _HTTPException
 from flask import (make_response, request, jsonify, Response,
                    Blueprint, current_app as app)
 
+from gn_auth.auth.requests import request_json
+
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_auth.auth.jwks import newest_jwk, jwks_directory
 
 from gn_auth.auth.authorisation.roles import Role
-from gn_auth.auth.authorisation.privileges import Privilege
-from gn_auth.auth.errors import InvalidData, InconsistencyError, AuthorisationError
+from gn_auth.auth.authorisation.roles.models import (
+    create_role,
+    user_resource_roles as _user_resource_roles)
+from gn_auth.auth.errors import (
+    InvalidData,
+    InconsistencyError,
+    AuthorisationError)
+from gn_auth.auth.authorisation.privileges import (
+    privilege_by_id,
+    privileges_by_ids)
 from gn_auth.auth.authorisation.roles.models import (
     role_by_id,
     db_rows_to_roles,
-    check_user_editable,
     delete_privilege_from_resource_role)
 
 from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
 from gn_auth.auth.authentication.users import User, user_by_id, user_by_email
 
-from .checks import authorised_for
+from .system.models import system_resource
+
+from .inbredset.views import popbp
+from .genotypes.views import genobp
+from .phenotypes.views import phenobp
+from .errors import MissingGroupError
+from .groups.models import Group, user_group
+from .checks import can_delete, authorised_for
 from .models import (
     Resource, resource_data, resource_by_id, public_resources,
     resource_categories, assign_resource_user, link_data_to_resource,
     unassign_resource_user, resource_category_by_id, user_roles_on_resources,
     unlink_data_from_resource, create_resource as _create_resource,
-    get_resource_id)
-from .groups.models import Group, resource_owner, group_role_by_id
+    get_resource_id, delete_resource as _delete_resource)
 
 resources = Blueprint("resources", __name__)
+resources.register_blueprint(popbp, url_prefix="/")
+resources.register_blueprint(genobp, url_prefix="/")
+resources.register_blueprint(phenobp, url_prefix="/")
 
 @resources.route("/categories", methods=["GET"])
 @require_oauth("profile group resource")
@@ -53,17 +73,23 @@ def list_resource_categories() -> Response:
 def create_resource() -> Response:
     """Create a new resource"""
     with require_oauth.acquire("profile group resource") as the_token:
-        form = request.form
+        form = request_json()
         resource_name = form.get("resource_name")
         resource_category_id = UUID(form.get("resource_category"))
         db_uri = app.config["AUTH_DB"]
         with db.connection(db_uri) as conn:
             try:
+                group = user_group(conn, the_token.user).maybe(
+                    False, lambda grp: grp)# type: ignore[misc, arg-type]
+                if not group:
+                    raise MissingGroupError(# Not all resources require an owner group
+                        "User with no group cannot create a resource.")
                 resource = _create_resource(
                     conn,
                     resource_name,
                     resource_category_by_id(conn, resource_category_id),
                     the_token.user,
+                    group,
                     (form.get("public") == "on"))
                 return jsonify(asdict(resource))
             except sqlite3.IntegrityError as sql3ie:
@@ -75,7 +101,9 @@ def create_resource() -> Response:
                     f"{type(sql3ie)=}: {sql3ie=}")
                 raise
 
+
 @resources.route("/view/<uuid:resource_id>")
+@resources.route("/<uuid:resource_id>/view")
 @require_oauth("profile group resource")
 def view_resource(resource_id: UUID) -> Response:
     """View a particular resource's details."""
@@ -112,7 +140,7 @@ def view_resource_data(resource_id: UUID) -> Response:
     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)
+        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(
@@ -126,9 +154,9 @@ def view_resource_data(resource_id: UUID) -> Response:
 def link_data():
     """Link group data to a specific resource."""
     try:
-        form = request.form
+        form = request_json()
         assert "resource_id" in form, "Resource ID not provided."
-        assert "data_link_id" in form, "Data Link ID not provided."
+        assert "data_link_ids" in form, "Data Link IDs not provided."
         assert "dataset_type" in form, "Dataset type not specified"
         assert form["dataset_type"].lower() in (
             "mrna", "genotype", "phenotype"), "Invalid dataset type provided."
@@ -136,8 +164,11 @@ def link_data():
         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(form["resource_id"]),
-                    form["dataset_type"], UUID(form["data_link_id"]))
+                    conn,
+                    the_token.user,
+                    UUID(form["resource_id"]),
+                    form["dataset_type"],
+                    tuple(UUID(dlinkid) for dlinkid in form["data_link_ids"]))
 
             return jsonify(with_db_connection(__link__))
     except AssertionError as aserr:
@@ -150,7 +181,7 @@ def link_data():
 def unlink_data():
     """Unlink data bound to a specific resource."""
     try:
-        form = request.form
+        form = request_json()
         assert "resource_id" in form, "Resource ID not provided."
         assert "data_link_id" in form, "Data Link ID not provided."
 
@@ -179,7 +210,7 @@ def resource_users(resource_id: UUID):
                 the_token.user,
                 ("group:resource:view-resource",),
                 (resource_id,))
-            systemlevelauth = __pk__authorised_for(
+            systemlevelauth = authorised_for(
                 conn,
                 the_token.user,
                 ("system:user:list",),
@@ -237,22 +268,25 @@ def resource_users(resource_id: UUID):
 @require_oauth("profile group resource role")
 def assign_role_to_user(resource_id: UUID) -> Response:
     """Assign a role on the specified resource to a user."""
-    with require_oauth.acquire("profile group resource role") as the_token:
+    with require_oauth.acquire("profile group resource role") as _token:
         try:
-            form = request.form
-            group_role_id = form.get("group_role_id", "")
+            form = request_json()
+            role_id = form.get("role_id", "")
             user_email = form.get("user_email", "")
-            assert bool(group_role_id), "The role must be provided."
+            assert bool(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)
+                authorised_for(
+                    conn,
+                    _token.user,
+                    ("resource:role:assign-role",),
+                    (resource_id,))
+                resource = resource_by_id(conn, _token.user, resource_id)
                 user = user_by_email(conn, user_email)
                 return assign_resource_user(
                     conn, resource, user,
-                    group_role_by_id(conn,
-                                     resource_owner(conn, resource),
-                                     UUID(group_role_id)))
+                    role_by_id(conn, UUID(role_id)))# type: ignore[arg-type]
         except AssertionError as aserr:
             raise AuthorisationError(aserr.args[0]) from aserr
 
@@ -262,21 +296,24 @@ def assign_role_to_user(resource_id: UUID) -> Response:
 @require_oauth("profile group resource role")
 def unassign_role_to_user(resource_id: UUID) -> Response:
     """Unassign a role on the specified resource from a user."""
-    with require_oauth.acquire("profile group resource role") as the_token:
+    with require_oauth.acquire("profile group resource role") as _token:
         try:
-            form = request.form
-            group_role_id = form.get("group_role_id", "")
+            form = request_json()
+            role_id = form.get("role_id", "")
             user_id = form.get("user_id", "")
-            assert bool(group_role_id), "The role must be provided."
+            assert bool(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)
+                authorised_for(
+                    conn,
+                    _token.user,
+                    ("resource:role:assign-role",),
+                    (resource_id,))
+                resource = resource_by_id(conn, _token.user, resource_id)
                 return unassign_resource_user(
                     conn, resource, user_by_id(conn, UUID(user_id)),
-                    group_role_by_id(conn,
-                                     resource_owner(conn, resource),
-                                     UUID(group_role_id)))
+                    role_by_id(conn, UUID(role_id)))# type: ignore[arg-type]
         except AssertionError as aserr:
             raise AuthorisationError(aserr.args[0]) from aserr
 
@@ -380,9 +417,18 @@ def resource_roles(resource_id: UUID) -> Response:
                     "ON rp.privilege_id=p.privilege_id "
                     "WHERE rr.resource_id=? AND rr.role_created_by=?",
                     (str(resource_id), str(_token.user.user_id)))
-                results = cursor.fetchall()
+                user_created = db_rows_to_roles(cursor.fetchall())
+
+                cursor.execute(
+                    "SELECT ur.user_id, ur.resource_id, 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 resource_id=? AND user_id=?",
+                    (str(resource_id), str(_token.user.user_id)))
+                assigned_to_user = db_rows_to_roles(cursor.fetchall())
 
-            return db_rows_to_roles(results)
+            return assigned_to_user + user_created
 
         return jsonify(with_db_connection(__roles__))
 
@@ -391,7 +437,7 @@ def resource_roles(resource_id: UUID) -> Response:
 def resources_authorisation():
     """Get user authorisations for given resource(s):"""
     try:
-        data = request.json
+        data = request_json()
         assert (data and "resource-ids" in data)
         resource_ids = tuple(UUID(resid) for resid in data["resource-ids"])
         pubres = tuple(
@@ -423,6 +469,14 @@ def resources_authorisation():
                 "Expected a JSON object with a 'resource-ids' key.")
         })
         resp.status_code = 400
+    except Exception as _exc:#pylint: disable=[broad-except]
+        app.logger.debug("Generic exception.", exc_info=True)
+        resp = jsonify({
+            "status": "general-exception",
+            "error_description": (
+                "Failed to fetch the user's privileges.")
+        })
+        resp.status_code = 500
 
     return resp
 
@@ -475,7 +529,8 @@ def get_user_roles_on_resource(name) -> Response:
             "email": _token.user.email,
             "roles": roles,
         }
-        token = jwt.encode(jose_header, payload, app.config["SSL_PRIVATE_KEY"])
+        token = jwt.encode(
+            jose_header, payload, newest_jwk(jwks_directory(app)))
         response.headers["Authorization"] = f"Bearer {token.decode('utf-8')}"
         return response
 
@@ -507,7 +562,7 @@ def resource_role(resource_id: UUID, role_id: UUID):
 
     _roles = db_rows_to_roles(results)
     if len(_roles) > 1:
-        msg = f"There is data corruption in the database."
+        msg = "There is data corruption in the database."
         return jsonify({
             "error": "RoleNotFound",
             "error_description": msg,
@@ -527,7 +582,6 @@ def unassign_resource_role_privilege(resource_id: UUID, role_id: UUID):
           db.connection(app.config["AUTH_DB"]) as conn,
           db.cursor(conn) as cursor):
         _role = role_by_id(conn, role_id)
-        # check_user_editable(_role) # Check whether role is user editable
 
         _authorised = authorised_for(
             conn,
@@ -539,14 +593,15 @@ def unassign_resource_role_privilege(resource_id: UUID, role_id: UUID):
                 "You are not authorised to edit/update this role.")
 
         # Actually unassign the privilege from the role
-        privilege_id = request.json.get("privilege_id")
+        privilege_id = request_json().get("privilege_id")
         if not privilege_id:
             raise AuthorisationError(
                 "You need to provide a privilege to unassign")
 
-        delete_privilege_from_resource_role(cursor,
-                                            _role,
-                                            privilege_by_id(privilege_id))
+        delete_privilege_from_resource_role(
+            cursor,
+            _role,# type: ignore[arg-type]
+            privilege_by_id(conn, privilege_id))# type: ignore[arg-type]
 
         return jsonify({
             "status": "Success",
@@ -570,3 +625,96 @@ def resource_role_users(resource_id: UUID, role_id: UUID):
         results = cursor.fetchall() or []
 
     return jsonify(tuple(User.from_sqlite3_row(row) for row in results)), 200
+
+
+@resources.route("/<uuid:resource_id>/roles/create", methods=["POST"])
+@require_oauth("profile group resource")
+def create_resource_role(resource_id: UUID):
+    """Create a role to act upon a specific resource."""
+    role_name = request_json().get("role_name", "").strip()
+    if not bool(role_name):
+        raise BadRequest("You must provide the name for the new role.")
+
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn,
+          db.cursor(conn) as cursor):
+        resource = resource_by_id(conn, _token.user, resource_id)
+        if not bool(resource):
+            raise BadRequest("No resource with that ID exists.")
+
+        privileges = privileges_by_ids(conn, request_json().get("privileges", []))
+        if len(privileges) == 0:
+            raise BadRequest(
+                "You must provide at least one privilege for the new role.")
+        role = create_role(cursor,
+                           f"{resource.resource_name}::{role_name}",
+                           privileges)
+        cursor.execute(
+            "INSERT INTO resource_roles(resource_id, role_created_by, role_id) "
+            "VALUES (:resource_id, :user_id, :role_id)",
+            {
+                "resource_id": str(resource_id),
+                "user_id": str(_token.user.user_id),
+                "role_id": str(role.role_id)
+            })
+
+    return jsonify(asdict(role))
+
+@resources.route("/<uuid:resource_id>/users/<uuid:user_id>/roles", methods=["GET"])
+@require_oauth("profile group resource role")
+def user_resource_roles(resource_id: UUID, user_id: UUID):
+    """Get a specific user's roles on a particular resource."""
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        if _token.user.user_id != user_id:
+            raise AuthorisationError(
+                "You are not authorised to view the roles this user has.")
+
+        _resource = resource_by_id(conn, _token.user, resource_id)
+        if not bool(_resource):
+            raise BadRequest("No resource was found with the given ID.")
+
+        return jsonify([asdict(role) for role in
+                        _user_resource_roles(conn, _token.user, _resource)])
+
+
+@resources.route("/delete", methods=["POST"])
+@require_oauth("profile group resource")
+def delete_resource():
+    """Delete the specified resource, if possible."""
+    with (require_oauth.acquire("profile group resource") as the_token,
+          db.connection(app.config["AUTH_DB"]) as conn):
+        form = request_json()
+        try:
+            resource_id = UUID(form.get("resource_id"))
+            if not can_delete(conn, the_token.user.user_id, resource_id):
+                raise AuthorisationError(
+                    "You are not allowed to delete this resource.")
+
+            data = resource_data(
+                conn,
+                resource_by_id(conn, the_token.user, resource_id),
+                0,
+                10)
+            if bool(data):
+                return jsonify({
+                    "error": "NonEmptyResouce",
+                    "error-description": "Cannot delete a resource with linked data"
+                }), 400
+
+            _delete_resource(conn, resource_id)
+            return jsonify({
+                "description": f"Successfully deleted resource with ID '{resource_id}'."
+            })
+        except ValueError as _verr:
+            app.logger.debug("Error!", exc_info=True)
+            return jsonify({
+                "error": "ValueError",
+                "error-description": "An invalid identifier was provided"
+            }), 400
+        except TypeError as _terr:
+            app.logger.debug("Error!", exc_info=True)
+            return jsonify({
+                "error": "TypeError",
+                "error-description": "An invalid identifier was provided"
+            }), 400