about summary refs log tree commit diff
path: root/gn_auth/auth/authorisation
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth/auth/authorisation')
-rw-r--r--gn_auth/auth/authorisation/data/genotypes.py41
-rw-r--r--gn_auth/auth/authorisation/data/mrna.py40
-rw-r--r--gn_auth/auth/authorisation/data/phenotypes.py81
-rw-r--r--gn_auth/auth/authorisation/data/views.py237
-rw-r--r--gn_auth/auth/authorisation/resources/base.py52
-rw-r--r--gn_auth/auth/authorisation/resources/checks.py41
-rw-r--r--gn_auth/auth/authorisation/resources/groups/models.py17
-rw-r--r--gn_auth/auth/authorisation/resources/models.py144
-rw-r--r--gn_auth/auth/authorisation/resources/system/views.py27
-rw-r--r--gn_auth/auth/authorisation/resources/views.py80
-rw-r--r--gn_auth/auth/authorisation/users/admin/views.py8
-rw-r--r--gn_auth/auth/authorisation/users/collections/views.py5
-rw-r--r--gn_auth/auth/authorisation/users/views.py22
13 files changed, 532 insertions, 263 deletions
diff --git a/gn_auth/auth/authorisation/data/genotypes.py b/gn_auth/auth/authorisation/data/genotypes.py
index ddb0add..d44cbfb 100644
--- a/gn_auth/auth/authorisation/data/genotypes.py
+++ b/gn_auth/auth/authorisation/data/genotypes.py
@@ -1,7 +1,9 @@
 """Handle linking of Genotype data to the Auth(entic|oris)ation system."""
 import uuid
-from dataclasses import asdict
+import logging
 from typing import Iterable
+from functools import reduce
+from dataclasses import asdict
 
 from gn_libs import mysqldb as gn3db
 from MySQLdb.cursors import DictCursor
@@ -11,6 +13,9 @@ from gn_auth.auth.db import sqlite3 as authdb
 from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.authorisation.resources.groups.models import Group
 
+
+logger = logging.getLogger(__name__)
+
 def linked_genotype_data(conn: authdb.DbConnection) -> Iterable[dict]:
     """Retrieve genotype data that is linked to user groups."""
     with authdb.cursor(conn) as cursor:
@@ -95,3 +100,37 @@ def link_genotype_data(
             "group": asdict(group),
             "datasets": datasets
         }
+
+
+def resources_by_datasets_and_traits(
+        authconn: authdb.DbConnection,
+        dsets_traits: tuple[tuple[str, str], ...]
+) -> tuple[dict, ...]:
+    """Fetch resources by their attached datasets and traits."""
+    traits_by_datasets: dict[str, tuple[str, ...]] = reduce(
+        lambda acc, curr: {
+            **acc,
+            curr[0]: acc.get(curr[0], tuple()) + (curr[1],)
+        },
+        dsets_traits,
+        {})
+    paramstr = ", ".join(["?"] * len(dsets_traits))
+    query = (
+        "SELECT r.*, rc.*, lgd.dataset_name FROM linked_genotype_data AS lgd "
+        "INNER JOIN genotype_resources AS mr ON lgd.data_link_id=mr.data_link_id "
+        "INNER JOIN resources AS r ON mr.resource_id=r.resource_id "
+        "INNER JOIN resource_categories AS rc "
+        "ON r.resource_category_id=rc.resource_category_id "
+        "WHERE lgd.dataset_name "
+        f"IN ({paramstr})")
+    logger.debug("QUERY: %s", query)
+    with authdb.cursor(authconn) as cursor:
+        params = tuple(traits_by_datasets.keys())
+        logger.debug("QUERY PARAMS: %s", params)
+        cursor.execute(query, tuple(traits_by_datasets.keys()))
+        return tuple({
+            "resource_id": row["resource_id"],
+            "resource_data": tuple(
+                f'{row["dataset_name"]}::{trait_id}'
+                for trait_id in traits_by_datasets[row["dataset_name"]])
+        } for row in cursor.fetchall())
diff --git a/gn_auth/auth/authorisation/data/mrna.py b/gn_auth/auth/authorisation/data/mrna.py
index 0cc644e..fcf6ea3 100644
--- a/gn_auth/auth/authorisation/data/mrna.py
+++ b/gn_auth/auth/authorisation/data/mrna.py
@@ -1,7 +1,9 @@
 """Handle linking of mRNA Assay data to the Auth(entic|oris)ation system."""
 import uuid
-from dataclasses import asdict
+import logging
 from typing import Iterable
+from functools import reduce
+from dataclasses import asdict
 
 from gn_libs import mysqldb as gn3db
 from MySQLdb.cursors import DictCursor
@@ -11,6 +13,10 @@ from gn_auth.auth.db import sqlite3 as authdb
 from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.authorisation.resources.groups.models import Group
 
+
+logger = logging.getLogger(__name__)
+
+
 def linked_mrna_data(conn: authdb.DbConnection) -> Iterable[dict]:
     """Retrieve mRNA Assay data that is linked to user groups."""
     with authdb.cursor(conn) as cursor:
@@ -100,3 +106,35 @@ def link_mrna_data(
             "group": asdict(group),
             "datasets": datasets
         }
+
+
+def resources_by_datasets_and_traits(
+        authconn: authdb.DbConnection,
+        dsets_traits: tuple[tuple[str, str], ...]
+) -> tuple[dict, ...]:
+    """Fetch resources by their attached datasets and traits."""
+    traits_by_datasets: dict[str, tuple[str, ...]] = reduce(
+        lambda acc, curr: {
+            **acc,
+            curr[0]: acc.get(curr[0], tuple()) + (curr[1],)
+        },
+        dsets_traits,
+        {})
+    paramstr = ", ".join(["?"] * len(dsets_traits))
+    query = (
+        "SELECT r.*, rc.*, lmd.dataset_name FROM linked_mrna_data AS lmd "
+        "INNER JOIN mrna_resources AS mr ON lmd.data_link_id=mr.data_link_id "
+        "INNER JOIN resources AS r ON mr.resource_id=r.resource_id "
+        "INNER JOIN resource_categories AS rc "
+        "ON r.resource_category_id=rc.resource_category_id "
+        "WHERE lmd.dataset_name "
+        f"IN ({paramstr})")
+    logger.debug("QUERY: %s", query)
+    with authdb.cursor(authconn) as cursor:
+        cursor.execute(query, tuple(traits_by_datasets.keys()))
+        return tuple({
+            "resource_id": row["resource_id"],
+            "resource_data": tuple(
+                f'{row["dataset_name"]}::{trait_id}'
+                for trait_id in traits_by_datasets[row["dataset_name"]])
+        } for row in cursor.fetchall())
diff --git a/gn_auth/auth/authorisation/data/phenotypes.py b/gn_auth/auth/authorisation/data/phenotypes.py
index 788b9e7..92cbe89 100644
--- a/gn_auth/auth/authorisation/data/phenotypes.py
+++ b/gn_auth/auth/authorisation/data/phenotypes.py
@@ -1,5 +1,7 @@
 """Handle linking of Phenotype data to the Auth(entic|oris)ation system."""
 import uuid
+import logging
+from functools import reduce
 from dataclasses import asdict
 from typing import Any, Iterable
 
@@ -11,7 +13,6 @@ from flask import request, jsonify, Response, Blueprint, current_app as app
 from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
 
 from gn_auth.auth.errors import AuthorisationError
-from gn_auth.auth.authorisation.checks import authorised_p
 from gn_auth.auth.authorisation.resources.checks import can_delete
 from gn_auth.auth.authorisation.resources.system.models import system_resource
 from gn_auth.auth.authorisation.resources.groups.models import Group, group_resource
@@ -20,8 +21,10 @@ from gn_auth.auth.authorisation.resources.groups.models import Group, group_reso
 from gn_auth.auth.authorisation.checks import require_json
 from gn_auth.auth.authorisation.resources.checks import authorised_for2
 
+logger = logging.getLogger(__name__)
 phenosbp = Blueprint("phenotypes", __name__)
 
+
 def linked_phenotype_data(
         authconn: authdb.DbConnection, gn3conn: gn3db.Connection,
         species: str = "") -> Iterable[dict[str, Any]]:
@@ -58,41 +61,6 @@ def linked_phenotype_data(
         gn3cursor.execute(query, params)
         return (item for item in gn3cursor.fetchall())
 
-@authorised_p(("system:data:link-to-group",),
-              error_description=(
-                  "You do not have sufficient privileges to link data to (a) "
-                  "group(s)."),
-              oauth2_scope="profile group resource")
-def ungrouped_phenotype_data(
-        authconn: authdb.DbConnection, gn3conn: gn3db.Connection):
-    """Retrieve phenotype data that is not linked to any user group."""
-    with gn3conn.cursor() as cursor:
-        params = tuple(
-            (row["SpeciesId"], row["InbredSetId"], row["PublishFreezeId"],
-             row["PublishXRefId"])
-            for row in linked_phenotype_data(authconn, gn3conn))
-        paramstr = ", ".join(["(?, ?, ?, ?)"] * len(params))
-        query = (
-            "SELECT spc.SpeciesId, spc.SpeciesName, iset.InbredSetId, "
-            "iset.InbredSetName, pf.Id AS PublishFreezeId, "
-            "pf.Name AS dataset_name, pf.FullName AS dataset_fullname, "
-            "pf.ShortName AS dataset_shortname, pxr.Id AS PublishXRefId "
-            "FROM "
-            "Species AS spc "
-            "INNER JOIN InbredSet AS iset "
-            "ON spc.SpeciesId=iset.SpeciesId "
-            "INNER JOIN PublishFreeze AS pf "
-            "ON iset.InbredSetId=pf.InbredSetId "
-            "INNER JOIN PublishXRef AS pxr "
-            "ON pf.InbredSetId=pxr.InbredSetId")
-        if len(params) > 0:
-            query = query + (
-                f" WHERE (iset.InbredSetId, pf.Id, pxr.Id) NOT IN ({paramstr})")
-
-        cursor.execute(query, params)
-        return tuple(dict(row) for row in cursor.fetchall())
-
-    return tuple()
 
 def pheno_traits_from_db(gn3conn: gn3db.Connection, params: tuple[dict, ...]) -> tuple[dict, ...]:
     """An internal utility function. Don't use outside of this module."""
@@ -274,3 +242,44 @@ def delete_linked_phenotypes_data(
             "requested": len(xref_ids),
             "deleted": _deleted
         })
+
+
+def __organise_resources_data__(acc, curr) -> dict:
+    logger.debug("ORGANISING... %s", dict(curr))
+    resource_row = acc.get(curr["resource_id"], {
+        "resource_id": curr["resource_id"],
+        "resource_data": tuple(),
+    })
+    return {
+        **acc,
+        curr["resource_id"]: {
+            **resource_row,
+            "resource_data": resource_row["resource_data"] + (
+                f'{curr["dataset_name"]}::{curr["trait_id"]}',)
+        }
+    }
+
+
+def resources_by_datasets_and_traits(
+        authconn: authdb.DbConnection,
+        dsets_traits: tuple[tuple[str, str], ...]
+) -> tuple[dict, ...]:
+    """Fetch resources by their attached datasets and traits."""
+    paramstr = ", ".join(["(?, ?)"] * len(dsets_traits))
+    query = (
+        "SELECT r.*, rc.*, lpd.dataset_name, lpd.PublishXRefId AS trait_id "
+        "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 r ON pr.resource_id=r.resource_id "
+        "INNER JOIN resource_categories AS rc "
+        "ON r.resource_category_id=rc.resource_category_id "
+        "WHERE (lpd.dataset_name, lpd.PublishXRefId) "
+        f"IN ({paramstr})")
+    with authdb.cursor(authconn) as cursor:
+        cursor.execute(
+            query, tuple(item for row in dsets_traits for item in row))
+        return tuple(reduce(
+            __organise_resources_data__,
+            cursor.fetchall(),
+            {}).values())
diff --git a/gn_auth/auth/authorisation/data/views.py b/gn_auth/auth/authorisation/data/views.py
index 4bf6746..584b239 100644
--- a/gn_auth/auth/authorisation/data/views.py
+++ b/gn_auth/auth/authorisation/data/views.py
@@ -2,9 +2,9 @@
 import sys
 import uuid
 import json
-from dataclasses import asdict
+import logging
 from typing import Any
-from functools import partial
+from functools import reduce, partial
 
 import redis
 from MySQLdb.cursors import DictCursor
@@ -13,6 +13,7 @@ from flask import request, jsonify, Response, Blueprint, current_app as app
 
 
 from gn_libs import mysqldb as gn3db
+from gn_libs import sqlite3 as db
 
 from gn_auth import jobs
 from gn_auth.commands import run_async_cmd
@@ -21,54 +22,32 @@ from gn_auth.auth.requests import request_json
 from gn_auth.auth.errors import InvalidData, NotFoundError
 from gn_auth.auth.authorisation.resources.groups.models import group_by_id
 
-from ...db import sqlite3 as db
-from ...db.sqlite3 import with_db_connection
+from gn_auth.auth.db.sqlite3 import with_db_connection # Replace this with gn_libs alternative
 
 from ..checks import require_json
 
-from ..users.models import user_resource_roles
-
-from ..resources.checks import authorised_for
-from ..resources.models import (
-    user_resources, public_resources, attach_resources_data)
-
 from ...authentication.users import User
 from ...authentication.oauth2.resource_server import require_oauth
 
-from .mrna import link_mrna_data, ungrouped_mrna_data
-from .genotypes import link_genotype_data, ungrouped_genotype_data
-from .phenotypes import phenosbp, link_phenotype_data, pheno_traits_from_db
-
+from .mrna import (
+    link_mrna_data,
+    ungrouped_mrna_data,
+    resources_by_datasets_and_traits as mrna_resources_by_datasets_and_traits)
+from .genotypes import (
+    link_genotype_data,
+    ungrouped_genotype_data,
+    resources_by_datasets_and_traits as geno_resources_by_datasets_and_traits)
+from .phenotypes import (
+    phenosbp,
+    link_phenotype_data,
+    pheno_traits_from_db,
+    resources_by_datasets_and_traits as pheno_resources_by_datasets_and_traits)
+
+
+logger = logging.getLogger(__name__)
 data = Blueprint("data", __name__)
 data.register_blueprint(phenosbp, url_prefix="/phenotypes")
 
-def build_trait_name(trait_fullname):
-    """
-    Initialises the trait's name, and other values from the search data provided
-
-    This is a copy of `gn3.db.traits.build_trait_name` function.
-    """
-    def dataset_type(dset_name):
-        if dset_name.find('Temp') >= 0:
-            return "Temp"
-        if dset_name.find('Geno') >= 0:
-            return "Geno"
-        if dset_name.find('Publish') >= 0:
-            return "Publish"
-        return "ProbeSet"
-
-    name_parts = trait_fullname.split("::")
-    assert len(name_parts) >= 2, f"Name format error: '{trait_fullname}'"
-    dataset_name = name_parts[0]
-    dataset_type = dataset_type(dataset_name)
-    return {
-        "db": {
-            "dataset_name": dataset_name,
-            "dataset_type": dataset_type},
-        "trait_fullname": trait_fullname,
-        "trait_name": name_parts[1],
-        "cellid": name_parts[2] if len(name_parts) == 3 else ""
-    }
 
 @data.route("species")
 def list_species() -> Response:
@@ -83,98 +62,116 @@ def list_species() -> Response:
 def authorisation() -> Response:
     """Retrieve the authorisation level for datasets/traits for the user."""
     # Access endpoint with something like:
-    # curl -X POST http://127.0.0.1:8080/api/oauth2/data/authorisation \
+    # curl -X POST http://127.0.0.1:8081/auth/data/authorisation \
     #    -H "Content-Type: application/json" \
     #    -d '{"traits": ["HC_M2_0606_P::1442370_at", "BXDGeno::01.001.695",
     #        "BXDPublish::10001"]}'
+    def __organise_traits__(acc, curr):
+        dset, _trt = curr
+        key = "ProbeSet"
+        if dset.endswith("Publish"):
+            key = "Publish"
+        elif dset.endswith("Geno"):
+            key="Geno"
+        elif dset.endswith("Temp"):
+            key = "Temp"
+        else:
+            key = "ProbeSet"
+
+        return {
+            **acc,
+            key: acc.get(key, tuple()) + (curr,)
+        }
+    _dset_traits: dict[str, tuple[tuple[str, str], ...]] = reduce(
+        __organise_traits__,
+        (
+            (dset.strip(), trt.strip()) for dset, trt in
+            (trtstr.split("::") for trtstr in
+             request_json().get("traits", []))),
+        {key: tuple() for key in ("Publish", "ProbeSet", "Geno", "Temp")})
+
     db_uri = app.config["AUTH_DB"]
-    privileges = {}
     user = User(uuid.uuid4(), "anon@ymous.user", "Anonymous User")
-    with db.connection(db_uri) as auth_conn:
+    with (db.connection(db_uri) as authconn, db.cursor(authconn) as cursor):
+        _all_resources = {
+            _rrow["resource_id"]: _rrow
+            for _rtypes in (
+                    pheno_resources_by_datasets_and_traits(
+                        authconn, _dset_traits["Publish"]),
+                    geno_resources_by_datasets_and_traits(
+                        authconn, _dset_traits["Geno"]),
+                    mrna_resources_by_datasets_and_traits(
+                        authconn, _dset_traits["ProbeSet"]))
+            for _rrow in _rtypes
+        }
+        if len(_all_resources.keys()) == 0:
+            raise NotFoundError(
+                "No resource(s) found for specified trait(s). Do(es) the "
+                "trait(s) actually exist?")
+        _resource_ids = tuple(_all_resources.keys())
+
+
+        def __explode_resource_data__(trait_fullname):
+            _dset, _trt = trait_fullname.split("::")
+            return {
+                "dataset_name": _dset,
+                "dataset_type": (
+                    "Phenotype" if _dset.endswith("Publish")
+                    else ("Genotype" if _dset.endswith("Geno")
+                          else ("Temporary" if _dset.endswith("Temp")
+                                else "mRNA"))),
+                "trait_name": _trt,
+                "trait_fullname": trait_fullname
+            }
+
+        _paramstr = ", ".join(["?"] * len(_resource_ids))
         try:
             with require_oauth.acquire("profile group resource") as _token:
                 user = _token.user
-                resources = attach_resources_data(
-                    auth_conn, user_resources(auth_conn, _token.user))
-                resources_roles = user_resource_roles(auth_conn, _token.user)
-                privileges = {
-                    resource_id: tuple(
-                        privilege.privilege_id
-                        for roles in resources_roles[resource_id]
-                        for privilege in roles.privileges)#("group:resource:view-resource",)
-                    for resource_id, is_authorised
-                    in authorised_for(
-                        auth_conn, _token.user,
-                        ("group:resource:view-resource",), tuple(
-                            resource.resource_id for resource in resources)).items()
-                    if is_authorised
-                }
+                cursor.execute(
+                    "SELECT ur.resource_id, r.role_id, rp.privilege_id "
+                    "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 "
+                    "WHERE ur.user_id = ? "
+                    f"AND ur.resource_id IN ({_paramstr})",
+                    (str(user.user_id),) + _resource_ids
+                )
+                _privileges_by_resource: dict[str, tuple[str, ...]] = reduce(
+                    lambda acc, curr: {
+                        **acc,
+                        curr["resource_id"]: (
+                            acc.get(curr["resource_id"], tuple())
+                            + (curr["privilege_id"],))
+                    },
+                    cursor.fetchall(),
+                    {})
         except _HTTPException as exc:
             err_msg = json.loads(exc.body)
             if err_msg["error"] == "missing_authorization":
-                resources = attach_resources_data(
-                    auth_conn, public_resources(auth_conn))
+                cursor.execute(
+                    "SELECT rsc.resource_id "
+                    "FROM resources AS rsc "
+                    "WHERE rsc.public = '1' "
+                    f"AND rsc.resource_id IN ({_paramstr}) ",
+                    _resource_ids)
+                _privileges_by_resource = {
+                    row["resource_id"]: ('group:resource:view-resource',)
+                    for row in cursor.fetchall()
+                }
             else:
                 raise exc from None
 
-        def __gen_key__(resource, data_item):
-            if resource.resource_category.resource_category_key.lower() == "phenotype":
-                return (
-                    f"{resource.resource_category.resource_category_key.lower()}::"
-                    f"{data_item['dataset_name']}::{data_item['PublishXRefId']}")
-            return (
-                f"{resource.resource_category.resource_category_key.lower()}::"
-                f"{data_item['dataset_name']}")
-
-        data_to_resource_map = {
-            __gen_key__(resource, data_item): resource.resource_id
-            for resource in resources
-            for data_item in resource.resource_data
-        }
-        privileges = {
-            **{
-                resource.resource_id: ("system:resource:public-read",)
-                for resource in resources if resource.public
-            },
-            **privileges}
-
-        args = request.get_json()
-        traits_names = args["traits"] # type: ignore[index]
-        def __translate__(val):
-            return {
-                "Temp": "Temp",
-                "ProbeSet": "mRNA",
-                "Geno": "Genotype",
-                "Publish": "Phenotype"
-            }[val]
-
-        def __trait_key__(trait):
-            dataset_type = __translate__(trait['db']['dataset_type']).lower()
-            dataset_name = trait["db"]["dataset_name"]
-            if dataset_type == "phenotype":
-                return f"{dataset_type}::{dataset_name}::{trait['trait_name']}"
-            return f"{dataset_type}::{dataset_name}"
-
-        return jsonify(tuple(
-            {
-                "user": asdict(user),
-                **{key:trait[key] for key in ("trait_fullname", "trait_name")},
-                "dataset_name": trait["db"]["dataset_name"],
-                "dataset_type": __translate__(trait["db"]["dataset_type"]),
-                "resource_id": data_to_resource_map.get(__trait_key__(trait)),
-                "privileges": privileges.get(
-                    data_to_resource_map.get(
-                        __trait_key__(trait),
-                        uuid.UUID("4afa415e-94cb-4189-b2c6-f9ce2b6a878d")),
-                    tuple()) + (
-                        # Temporary traits do not exist in db: Set them
-                        # as public-read
-                        ("system:resource:public-read",)
-                        if trait["db"]["dataset_type"] == "Temp"
-                        else tuple())
-            } for trait in
-            (build_trait_name(trait_fullname)
-             for trait_fullname in traits_names)))
+        return jsonify({
+            "authorisation": [{
+                **resource,
+                "resource_data": [
+                    __explode_resource_data__(item)
+                            for item in resource["resource_data"]],
+                "privileges": _privileges_by_resource.get(resource["resource_id"], tuple())
+            } for resource in _all_resources.values()]
+        })
+
 
 def __search_mrna__():
     query = __request_key__("query", "")
@@ -219,7 +216,7 @@ def __search_phenotypes__():
         job_id = uuid.uuid4()
         selected = __request_key__("selected_traits", [])
         command =[
-            sys.executable, "-m", "scripts.search_phenotypes",
+            sys.executable, "-m", "gn_auth.scripts.search_phenotypes",
             __request_key__("species_name"),
             __request_key__("query"),
             str(job_id),
diff --git a/gn_auth/auth/authorisation/resources/base.py b/gn_auth/auth/authorisation/resources/base.py
index 333ba0d..e4a1239 100644
--- a/gn_auth/auth/authorisation/resources/base.py
+++ b/gn_auth/auth/authorisation/resources/base.py
@@ -1,10 +1,17 @@
 """Base types for resources."""
+import logging
+import datetime
 from uuid import UUID
 from dataclasses import dataclass
-from typing import Any, Sequence
+from typing import Any, Sequence, Optional
 
 import sqlite3
 
+from gn_auth.auth.authentication.users import User
+
+
+logger = logging.getLogger(__name__)
+
 
 @dataclass(frozen=True)
 class ResourceCategory:
@@ -22,10 +29,49 @@ class Resource:
     resource_category: ResourceCategory
     public: bool
     resource_data: Sequence[dict[str, Any]] = tuple()
+    created_by: Optional[User] = None
+    created_at: datetime.datetime = datetime.datetime(1970, 1, 1, 0, 0, 0)
+
+    @staticmethod
+    def from_resource(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+            resource,
+            resource_id: Optional[UUID] = None,
+            resource_name: Optional[str] = None,
+            resource_category: Optional[ResourceCategory] = None,
+            public: Optional[bool] = None,
+            resource_data: Optional[Sequence[dict[str, Any]]] = None,
+            created_by: Optional[User] = None,
+            created_at: Optional[datetime.datetime] = None
+    ):
+        """Takes a Resource object `resource` and updates the attributes specified in `kwargs`."""
+        return Resource(
+            resource_id=resource_id or resource.resource_id,
+            resource_name=resource_name or resource.resource_name,
+            resource_category=resource_category or resource.resource_category,
+            public=bool(public) or resource.public,
+            resource_data=resource_data or resource.resource_data,
+            created_by=created_by or resource.created_by,
+            created_at=created_at or resource.created_at)
 
 
 def resource_from_dbrow(row: sqlite3.Row):
     """Convert an SQLite3 resultset row into a resource."""
+    try:
+        created_at = datetime.datetime.fromtimestamp(row["created_at"])
+    except IndexError as _ie:
+        created_at = datetime.datetime(1970, 1, 1, 0, 0, 0)
+
+    try:
+        created_by = User.from_sqlite3_row({
+            "user_id": row["creator_user_id"],
+            "email": row["creator_email"],
+            "name": row["creator_name"],
+            "verified": row["creator_verified"],
+            "created": row["creator_created"]
+        })
+    except IndexError as _ie:
+        created_by = None
+
     return Resource(
         resource_id=UUID(row["resource_id"]),
         resource_name=row["resource_name"],
@@ -33,4 +79,6 @@ def resource_from_dbrow(row: sqlite3.Row):
             UUID(row["resource_category_id"]),
             row["resource_category_key"],
             row["resource_category_description"]),
-        public=bool(int(row["public"])))
+        public=bool(int(row["public"])),
+        created_by=created_by,
+        created_at=created_at)
diff --git a/gn_auth/auth/authorisation/resources/checks.py b/gn_auth/auth/authorisation/resources/checks.py
index bc9e4da..252df2f 100644
--- a/gn_auth/auth/authorisation/resources/checks.py
+++ b/gn_auth/auth/authorisation/resources/checks.py
@@ -135,6 +135,11 @@ def can_delete(
         resource_id: uuid.UUID
 ) -> bool:
     """Check whether user is allowed delete a resource and/or its data."""
+    warnings.warn(
+        (f"Function '{__name__}.can_delete' is deprecated. "
+         "Use `gn_libs.privileges.resources.can_delete` instead."),
+        category=DeprecationWarning,
+        stacklevel=2)
     return (
         authorised_for_spec(# resource-level delete access
             conn,
@@ -149,42 +154,17 @@ def can_delete(
             "(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."""
+    warnings.warn(
+        (f"Function '{__name__}.can_edit' is deprecated. "
+         "Use `gn_libs.privileges.resources.can_edit` instead."),
+        category=DeprecationWarning,
+        stacklevel=2)
     return (
         authorised_for_spec(
             # resource-level edit access: user has edit access to his resource.
@@ -199,4 +179,3 @@ def can_edit(
             user_id,
             system_resource(conn).resource_id,
             "(OR system:system-wide:data:edit system:resource:edit)"))
-
diff --git a/gn_auth/auth/authorisation/resources/groups/models.py b/gn_auth/auth/authorisation/resources/groups/models.py
index 6a7af4c..07e6dbe 100644
--- a/gn_auth/auth/authorisation/resources/groups/models.py
+++ b/gn_auth/auth/authorisation/resources/groups/models.py
@@ -1,5 +1,6 @@
 """Handle the management of resource/user groups."""
 import json
+import datetime
 from uuid import UUID, uuid4
 from functools import reduce
 from dataclasses import dataclass
@@ -100,8 +101,12 @@ def user_membership(conn: db.DbConnection, user: User) -> Sequence[Group]:
         "create a new group."),
     oauth2_scope="profile group")
 def create_group(
-        conn: db.DbConnection, group_name: str, group_leader: User,
-        group_description: Optional[str] = None) -> Group:
+        conn: db.DbConnection,
+        group_name: str,
+        group_leader: User,
+        group_description: Optional[str] = None,
+        creator: Optional[User] = None
+) -> Group:
     """Create a new group."""
     def resource_category_by_key(
             cursor: db.DbCursor, category_key: str):
@@ -134,11 +139,15 @@ def create_group(
                 resource_category_by_key(
                     cursor, "group")["resource_category_id"]
             ),
-            "public": 0
+            "public": 0,
+            "created_by": str(
+                creator.user_id if creator else group_leader.user_id),
+            "created_at": datetime.datetime.now().timestamp()
         }
         cursor.execute(
             "INSERT INTO resources VALUES "
-            "(:resource_id, :resource_name, :resource_category_id, :public)",
+            "(:resource_id, :resource_name, :resource_category_id, :public, "
+            ":created_by, :created_at)",
             _group_resource)
         cursor.execute(
             "INSERT INTO group_resources(resource_id, group_id) "
diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py
index a4df363..27ef183 100644
--- a/gn_auth/auth/authorisation/resources/models.py
+++ b/gn_auth/auth/authorisation/resources/models.py
@@ -1,4 +1,6 @@
 """Handle the management of resources."""
+import logging
+from datetime import datetime
 from dataclasses import asdict
 from uuid import UUID, uuid4
 from functools import reduce, partial
@@ -14,10 +16,9 @@ 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 .system.models import system_resource
-from .checks import authorised_for, authorised_for_spec
+from .common import assign_resource_owner_role
+from .checks import can_edit, 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 (
@@ -37,6 +38,9 @@ from .phenotypes.models import (
     unlink_data_from_resource as phenotype_unlink_data_from_resource)
 
 
+logger = logging.getLogger(__name__)
+
+
 @authorised_p(("group:resource:create-resource",),
               error_description="Insufficient privileges to create a resource",
               oauth2_scope="profile resource")
@@ -46,17 +50,20 @@ def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-a
         resource_category: ResourceCategory,
         user: User,
         group: Group,
-        public: bool
+        public: bool,
+        created_at: datetime = datetime.now()
 ) -> Resource:
     """Create a resource item."""
     def __create_resource__(cursor: db.DbCursor) -> Resource:
         resource = Resource(uuid4(), resource_name, resource_category, public)
         cursor.execute(
-            "INSERT INTO resources VALUES (?, ?, ?, ?)",
+            "INSERT INTO resources VALUES (?, ?, ?, ?, ?, ?)",
             (str(resource.resource_id),
              resource_name,
              str(resource.resource_category.resource_category_id),
-             1 if resource.public else 0))
+             1 if resource.public else 0,
+             str(user.user_id),
+             created_at.timestamp()))
         # 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
@@ -71,8 +78,6 @@ def create_resource(# pylint: disable=[too-many-arguments, too-many-positional-a
                        "VALUES (?, ?)",
                        (str(group.group_id), str(resource.resource_id)))
         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
 
@@ -98,6 +103,27 @@ def delete_resource(conn: db.DbConnection, resource_id: UUID):
                        (str(resource_id),))
 
 
+def edit_resource(conn: db.DbConnection, resource_id: UUID, name: str) -> Resource:
+    """Edit basic resource details."""
+    with db.cursor(conn) as cursor:
+        cursor.execute("UPDATE resources SET resource_name=? "
+                       "WHERE resource_id=?",
+                       (name, str(resource_id)))
+        cursor.execute(
+            "SELECT r.*, rc.* FROM resources AS r "
+            "INNER JOIN resource_categories AS rc "
+            "ON r.resource_category_id=rc.resource_category_id "
+            "WHERE r.resource_id=?",
+            (str(resource_id),))
+        _resource = resource_from_dbrow(cursor.fetchone())
+        cursor.execute(
+            "SELECT u.* FROM resources AS r INNER JOIN users AS u "
+            "ON r.created_by=u.user_id WHERE r.resource_id=?",
+            (str(resource_id),))
+        return Resource.from_resource(
+            _resource, created_by=User.from_sqlite3_row(cursor.fetchone()))
+
+
 def resource_category_by_id(
         conn: db.DbConnection, category_id: UUID) -> ResourceCategory:
     """Retrieve a resource category by its ID."""
@@ -125,6 +151,18 @@ def resource_categories(conn: db.DbConnection) -> Sequence[ResourceCategory]:
             for row in cursor.fetchall())
     return tuple()
 
+
+def __fetch_creators__(cursor, creators_ids: tuple[str, ...]):
+    cursor.execute(
+            ("SELECT * FROM users "
+             f"WHERE user_id IN ({', '.join(['?'] * len(creators_ids))})"),
+            creators_ids)
+    return {
+        row["user_id"]: User.from_sqlite3_row(row)
+        for row in cursor.fetchall()
+    }
+
+
 def public_resources(conn: db.DbConnection) -> Sequence[Resource]:
     """List all resources marked as public"""
     categories = {
@@ -132,10 +170,19 @@ def public_resources(conn: db.DbConnection) -> Sequence[Resource]:
     }
     with db.cursor(conn) as cursor:
         cursor.execute("SELECT * FROM resources WHERE public=1")
-        results = cursor.fetchall()
+        resource_rows = tuple(cursor.fetchall())
+        _creators_ = __fetch_creators__(
+            cursor, tuple(row["created_by"] for row in resource_rows))
         return tuple(
-            Resource(UUID(row[0]), row[1], categories[row[2]], bool(row[3]))
-            for row in results)
+            Resource(
+                UUID(row[0]),
+                row[1],
+                categories[row[2]],
+                bool(row[3]),
+                created_by=_creators_[row["created_by"]],
+                created_at=datetime.fromtimestamp(row["created_at"]))
+            for row in resource_rows)
+
 
 def group_leader_resources(
         conn: db.DbConnection, user: User, group: Group,
@@ -155,22 +202,63 @@ def group_leader_resources(
                 for row in cursor.fetchall())
     return tuple()
 
-def user_resources(conn: db.DbConnection, user: User) -> Sequence[Resource]:
+
+def user_resources(
+        conn: db.DbConnection,
+        user: User,
+        start_at: int = 0,
+        count: int = 0,
+        text_filter: str = ""
+) -> tuple[Sequence[Resource], int]:
     """List the resources available to the user"""
-    with db.cursor(conn) as cursor:
-        cursor.execute(
-            ("SELECT DISTINCT(r.resource_id), r.resource_name,  "
-             "r.resource_category_id, r.public, rc.resource_category_key, "
-             "rc.resource_category_description "
+    text_filter = text_filter.strip()
+    query_template = ("SELECT %%COLUMNS%%  "
              "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=?"),
+             "WHERE ur.user_id=? %%LIKE%% %%LIMITS%%")
+    with db.cursor(conn) as cursor:
+        cursor.execute(
+            query_template.replace(
+                "%%COLUMNS%%", "COUNT(DISTINCT(r.resource_id)) AS count"
+            ).replace(
+                "%%LIKE%%", ""
+            ).replace(
+                "%%LIMITS%%", ""),
             (str(user.user_id),))
+        _total_records = int(cursor.fetchone()["count"])
+        cursor.execute(
+            query_template.replace(
+                "%%COLUMNS%%",
+                "DISTINCT(r.resource_id), r.resource_name, "
+                "r.resource_category_id, r.public, r.created_by, r.created_at, "
+                "rc.resource_category_key, rc.resource_category_description"
+            ).replace(
+                "%%LIKE%%",
+                ("" if text_filter == "" else (
+                    "AND (r.resource_name LIKE ? OR "
+                    "rc.resource_category_key LIKE ? OR "
+                    "rc.resource_category_description LIKE ? )"))
+            ).replace(
+                "%%LIMITS%%",
+                ("" if count <= 0 else f"LIMIT {count} OFFSET {start_at}")),
+            (str(user.user_id),) + (
+                tuple() if text_filter == "" else
+                tuple(f"%{text_filter}%" for _ in range(0, 3))
+            ))
         rows = cursor.fetchall() or []
 
-    return tuple(resource_from_dbrow(row) for row in rows)
+        _creators_ = __fetch_creators__(
+            cursor, tuple(row["created_by"] for row in rows))
+
+    return tuple(
+        Resource.from_resource(
+            resource_from_dbrow(row),
+            created_by=_creators_[row["created_by"]],
+            created_at=datetime.fromtimestamp(row["created_at"])
+        ) for row in rows), _total_records
+
 
 
 def resource_data(conn, resource, offset: int = 0, limit: Optional[int] = None) -> tuple[dict, ...]:
@@ -243,12 +331,9 @@ def link_data_to_resource(
         data_link_ids: tuple[UUID, ...]
 ) -> tuple[dict, ...]:
     """Link data to resource."""
-    if not authorised_for(
-            conn, user, ("group:resource:edit-resource",),
-            (resource_id,))[resource_id]:
+    if not can_edit(conn, user.user_id, resource_id):
         raise AuthorisationError(
-            "You are not authorised to link data to resource with id "
-            f"{resource_id}")
+            "You are not authorised to link/unlink data to this resource.")
 
     resource = with_db_connection(partial(
         resource_by_id, user=user, resource_id=resource_id))
@@ -261,12 +346,9 @@ def link_data_to_resource(
 def unlink_data_from_resource(
         conn: db.DbConnection, user: User, resource_id: UUID, data_link_id: UUID):
     """Unlink data from resource."""
-    if not authorised_for(
-            conn, user, ("group:resource:edit-resource",),
-            (resource_id,))[resource_id]:
+    if not can_edit(conn, user.user_id, resource_id):
         raise AuthorisationError(
-            "You are not authorised to link data to resource with id "
-            f"{resource_id}")
+            "You are not authorised to link/unlink data this resource.")
 
     resource = with_db_connection(partial(
         resource_by_id, user=user, resource_id=resource_id))
@@ -359,9 +441,7 @@ def save_resource(
         conn: db.DbConnection, user: User, resource: Resource) -> Resource:
     """Update an existing resource."""
     resource_id = resource.resource_id
-    authorised = authorised_for(
-        conn, user, ("group:resource:edit-resource",), (resource_id,))
-    if authorised[resource_id]:
+    if can_edit(conn, user.user_id, resource_id):
         with db.cursor(conn) as cursor:
             cursor.execute(
                 "UPDATE resources SET "
diff --git a/gn_auth/auth/authorisation/resources/system/views.py b/gn_auth/auth/authorisation/resources/system/views.py
index b0d40c2..d7a57a9 100644
--- a/gn_auth/auth/authorisation/resources/system/views.py
+++ b/gn_auth/auth/authorisation/resources/system/views.py
@@ -1,19 +1,34 @@
 """Views relating to `System` resource(s)."""
+import logging
 from dataclasses import asdict
-from flask import jsonify, Blueprint
+from flask import request, jsonify, Blueprint, current_app as app
 
-from gn_auth.auth.db.sqlite3 import with_db_connection
+from gn_libs import sqlite3 as authdb
 
+from gn_auth.auth.authorisation.roles.models import db_rows_to_roles
 from gn_auth.auth.authentication.oauth2.resource_server import require_oauth
 
 from .models import user_roles_on_system
 
+logger = logging.getLogger(__name__)
 system = Blueprint("system", __name__)
 
+
 @system.route("/roles")
 def system_roles():
     """Get the roles that a user has that act on the system."""
-    with require_oauth.acquire("profile group") as the_token:
-        roles = with_db_connection(
-            lambda conn: user_roles_on_system(conn, the_token.user))
-        return jsonify(tuple(asdict(role) for role in roles))
+    with (authdb.connection(app.config["AUTH_DB"]) as conn,
+          authdb.cursor(conn) as cursor):
+        if not bool(request.headers.get("Authorization", False)):
+            cursor.execute(
+                "SELECT r.*, p.* FROM roles AS r "
+                "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 r.role_name='public-view'")
+            return jsonify(tuple(
+                asdict(role) for role in db_rows_to_roles(cursor.fetchall())))
+
+        with require_oauth.acquire("profile group") as the_token:
+            return jsonify(tuple(
+                asdict(role) for role in
+                user_roles_on_system(conn, the_token.user)))
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index e4401c5..f114476 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -1,9 +1,10 @@
 """The views/routes for the resources package"""
-from uuid import UUID, uuid4
+import time
 import json
+import logging
 import operator
 import sqlite3
-import time
+from uuid import UUID, uuid4
 
 from dataclasses import asdict
 from functools import reduce
@@ -13,6 +14,7 @@ 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)
+import gn_libs.privileges.resources
 
 from gn_auth.auth.requests import request_json
 
@@ -39,12 +41,11 @@ from gn_auth.auth.authorisation.roles.models import (
 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 .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 .system.models import system_resource
 from .groups.models import Group, user_group
 from .checks import can_delete, authorised_for
 from .models import (
@@ -52,7 +53,10 @@ from .models import (
     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, delete_resource as _delete_resource)
+    get_resource_id, delete_resource as _delete_resource,
+    edit_resource as _edit_resource)
+
+logger = logging.getLogger(__name__)
 
 resources = Blueprint("resources", __name__)
 resources.register_blueprint(popbp, url_prefix="/")
@@ -97,8 +101,7 @@ def create_resource() -> Response:
                                       "resources.resource_name"):
                     raise InconsistencyError(
                         "You cannot have duplicate resource names.") from sql3ie
-                app.logger.debug(
-                    f"{type(sql3ie)=}: {sql3ie=}")
+                logger.debug("type(sql3ie)=%s: sql3ie=%s", type(sql3ie), sql3ie)
                 raise
 
 
@@ -116,6 +119,49 @@ def view_resource(resource_id: UUID) -> Response:
                 )
             )
 
+
+@resources.route("/<uuid:resource_id>/edit", methods=["POST"])
+@require_oauth("profile group resource")
+def edit_resource(resource_id: UUID) -> Response:
+    """Update/edit basic details regarding a resource."""
+    db_uri = app.config["AUTH_DB"]
+    with (require_oauth.acquire("profile group resource") as _token,
+          db.connection(db_uri) as conn):
+        def __extract_privileges__(roles: tuple[Role, ...]) -> tuple[str, ...]:
+            return tuple(
+                priv.privilege_id for role in roles
+                for priv in role.privileges)
+
+        _sys_resource = system_resource(conn)
+        _privileges = {
+            ("system_privileges"
+             if _rid == _sys_resource.resource_id
+             else "resource_privileges"): __extract_privileges__(_rroles)
+            for _rid, _rroles in user_roles_on_resources(
+                conn,
+                _token.user,
+                (resource_id, _sys_resource.resource_id)
+            ).items()
+        }
+        if not gn_libs.privileges.resources.can_edit(**_privileges):
+            return make_response(jsonify({
+                "error": "AuthorisationError",
+                "error_description": "You are not allowed to edit this resource."
+            }), 401)
+
+        name = (request_json().get("resource_name") or "").strip()
+        if bool(name):
+            return jsonify({
+                "resource": asdict(_edit_resource(conn, resource_id, name)),
+                "message": "Resource updated successfully",
+                "status": "success"
+            })
+
+        return make_response(jsonify({
+            "error_description": "Expected `resource_name` to be provided.",
+            "error": "InvalidInput"
+        }), 400)
+
 def __safe_get_requests_page__(key: str = "page") -> int:
     """Get the results page if it exists or default to the first page."""
     try:
@@ -234,9 +280,11 @@ def resource_users(resource_id: UUID):
                             **users_n_roles,
                             user_id: {
                                 "user": user,
-                                "user_group": Group(
-                                    UUID(row["group_id"]), row["group_name"],
-                                    json.loads(row["group_metadata"])),
+                                "user_group": (
+                                    Group(UUID(row["group_id"]),
+                                          row["group_name"],
+                                          json.loads(row["group_metadata"]))
+                                    if bool(row["group_id"]) else False) ,
                                 "roles": users_n_roles.get(
                                     user_id, {}).get("roles", tuple()) + (role,)
                             }
@@ -244,7 +292,7 @@ def resource_users(resource_id: UUID):
                     cursor.execute(
                         "SELECT g.*, u.*, r.* "
                         "FROM groups AS g INNER JOIN group_users AS gu "
-                        "ON g.group_id=gu.group_id INNER JOIN users AS u "
+                        "ON g.group_id=gu.group_id RIGHT JOIN users AS u "
                         "ON gu.user_id=u.user_id INNER JOIN user_roles AS ur "
                         "ON u.user_id=ur.user_id INNER JOIN roles AS r "
                         "ON ur.role_id=r.role_id "
@@ -257,7 +305,8 @@ def resource_users(resource_id: UUID):
         results = (
             {
                 "user": asdict(row["user"]),
-                "user_group": asdict(row["user_group"]),
+                "user_group": (
+                    asdict(row["user_group"]) if row["user_group"] else False),
                 "roles": tuple(asdict(role) for role in row["roles"])
             } for row in (
                 user_row for user_id, user_row
@@ -470,7 +519,7 @@ def resources_authorisation():
         })
         resp.status_code = 400
     except Exception as _exc:#pylint: disable=[broad-except]
-        app.logger.debug("Generic exception.", exc_info=True)
+        logger.debug("Generic exception.", exc_info=True)
         resp = jsonify({
             "status": "general-exception",
             "error_description": (
@@ -508,7 +557,6 @@ def get_user_roles_on_resource(name) -> Response:
         response = make_response({
             # Flatten this list
             "roles": roles,
-            "silly": "ausah",
         })
         iat = int(time.time())
         jose_header = {
@@ -707,13 +755,13 @@ def delete_resource():
                 "description": f"Successfully deleted resource with ID '{resource_id}'."
             })
         except ValueError as _verr:
-            app.logger.debug("Error!", exc_info=True)
+            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)
+            logger.debug("Error!", exc_info=True)
             return jsonify({
                 "error": "TypeError",
                 "error-description": "An invalid identifier was provided"
diff --git a/gn_auth/auth/authorisation/users/admin/views.py b/gn_auth/auth/authorisation/users/admin/views.py
index 9bc1c36..62eccfd 100644
--- a/gn_auth/auth/authorisation/users/admin/views.py
+++ b/gn_auth/auth/authorisation/users/admin/views.py
@@ -1,6 +1,5 @@
 """UI for admin stuff"""
 import uuid
-import json
 import random
 import string
 from typing import Optional
@@ -240,13 +239,6 @@ def register_client():
         client_secret = raw_client_secret)
 
 
-def __parse_client__(sqlite3_row) -> dict:
-    """Parse the client details into python datatypes."""
-    return {
-        **dict(sqlite3_row),
-        "client_metadata": json.loads(sqlite3_row["client_metadata"])
-    }
-
 @admin.route("/list-client", methods=["GET"])
 @is_admin
 def list_clients():
diff --git a/gn_auth/auth/authorisation/users/collections/views.py b/gn_auth/auth/authorisation/users/collections/views.py
index f619c3d..5ed2c23 100644
--- a/gn_auth/auth/authorisation/users/collections/views.py
+++ b/gn_auth/auth/authorisation/users/collections/views.py
@@ -1,4 +1,5 @@
 """Views regarding user collections."""
+import logging
 from uuid import UUID
 
 from redis import Redis
@@ -25,8 +26,10 @@ from .models import (
     REDIS_COLLECTIONS_KEY,
     delete_collections as _delete_collections)
 
+logger = logging.getLogger(__name__)
 collections = Blueprint("collections", __name__)
 
+
 @collections.route("/list")
 @require_oauth("profile user")
 def list_user_collections() -> Response:
@@ -44,7 +47,7 @@ def list_anonymous_collections(anon_id: UUID) -> Response:
         def __list__(conn: db.DbConnection) -> tuple:
             try:
                 _user = user_by_id(conn, anon_id)
-                current_app.logger.warning(
+                logger.warning(
                     "Fetch collections for authenticated user using the "
                     "`list_user_collections()` endpoint.")
                 return tuple()
diff --git a/gn_auth/auth/authorisation/users/views.py b/gn_auth/auth/authorisation/users/views.py
index c248ac3..a706067 100644
--- a/gn_auth/auth/authorisation/users/views.py
+++ b/gn_auth/auth/authorisation/users/views.py
@@ -1,5 +1,6 @@
 """User authorisation endpoints."""
 import uuid
+import logging
 import sqlite3
 import secrets
 import traceback
@@ -57,6 +58,8 @@ from .models import list_users
 from .masquerade.views import masq
 from .collections.views import collections
 
+logger = logging.getLogger(__name__)
+
 users = Blueprint("users", __name__)
 users.register_blueprint(masq, url_prefix="/masquerade")
 users.register_blueprint(collections, url_prefix="/collections")
@@ -235,11 +238,11 @@ def register_user() -> Response:
                                         redirect_uri=form["redirect_uri"])
                 return jsonify(asdict(user))
         except sqlite3.IntegrityError as sq3ie:
-            current_app.logger.error(traceback.format_exc())
+            logger.error(traceback.format_exc())
             raise UserRegistrationError(
                 "A user with that email already exists") from sq3ie
         except EmailNotValidError as enve:
-            current_app.logger.error(traceback.format_exc())
+            logger.error(traceback.format_exc())
             raise(UserRegistrationError(f"Email Error: {str(enve)}")) from enve
 
     raise Exception(# pylint: disable=[broad-exception-raised]
@@ -317,12 +320,21 @@ def user_group() -> Response:
 @require_oauth("profile resource")
 def user_resources() -> Response:
     """Retrieve the resources a user has access to."""
+    _request_params = request_json()
     with require_oauth.acquire("profile resource") as the_token:
         db_uri = current_app.config["AUTH_DB"]
         with db.connection(db_uri) as conn:
-            return jsonify([
-                asdict(resource) for resource in
-                _user_resources(conn, the_token.user)])
+            _resources, _total_records = _user_resources(
+                conn,
+                the_token.user,
+                start_at=int(_request_params.get("start", 0)),
+                count=int(_request_params.get("length", 0)),
+                text_filter=_request_params.get("text_filter", ""))
+            return jsonify({
+                "resources": [asdict(resource) for resource in _resources],
+                "total-records": _total_records,
+                "filtered-records": len(_resources)
+            })
 
 @users.route("group/join-request", methods=["GET"])
 @require_oauth("profile group")