about summary refs log tree commit diff
path: root/gn_auth
diff options
context:
space:
mode:
Diffstat (limited to 'gn_auth')
-rw-r--r--gn_auth/auth/authentication/users.py4
-rw-r--r--gn_auth/auth/authorisation/data/views.py2
-rw-r--r--gn_auth/auth/authorisation/resources/base.py52
-rw-r--r--gn_auth/auth/authorisation/resources/checks.py1
-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.py61
-rw-r--r--gn_auth/auth/authorisation/users/views.py22
-rw-r--r--gn_auth/auth/errors.py2
-rw-r--r--gn_auth/errors/authlib.py2
11 files changed, 268 insertions, 66 deletions
diff --git a/gn_auth/auth/authentication/users.py b/gn_auth/auth/authentication/users.py
index 140ce36..fded79f 100644
--- a/gn_auth/auth/authentication/users.py
+++ b/gn_auth/auth/authentication/users.py
@@ -1,6 +1,6 @@
 """User-specific code and data structures."""
 import datetime
-from typing import Tuple
+from typing import Tuple, Union
 from uuid import UUID, uuid4
 from dataclasses import dataclass
 
@@ -26,7 +26,7 @@ class User:
         return self.user_id
 
     @staticmethod
-    def from_sqlite3_row(row: sqlite3.Row):
+    def from_sqlite3_row(row: Union[sqlite3.Row, dict]):
         """Generate a user from a row in an SQLite3 resultset"""
         return User(user_id=UUID(row["user_id"]),
                     email=row["email"],
diff --git a/gn_auth/auth/authorisation/data/views.py b/gn_auth/auth/authorisation/data/views.py
index 4bf6746..1526070 100644
--- a/gn_auth/auth/authorisation/data/views.py
+++ b/gn_auth/auth/authorisation/data/views.py
@@ -95,7 +95,7 @@ def authorisation() -> Response:
             with require_oauth.acquire("profile group resource") as _token:
                 user = _token.user
                 resources = attach_resources_data(
-                    auth_conn, user_resources(auth_conn, _token.user))
+                    auth_conn, user_resources(auth_conn, _token.user)[0])
                 resources_roles = user_resource_roles(auth_conn, _token.user)
                 privileges = {
                     resource_id: tuple(
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..004c780 100644
--- a/gn_auth/auth/authorisation/resources/checks.py
+++ b/gn_auth/auth/authorisation/resources/checks.py
@@ -199,4 +199,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..ab44795 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,43 @@ 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):
+        _privileges = tuple(
+            privilege.privilege_id
+            for role in (
+                    role for resource in user_roles_on_resources(
+                        conn,
+                        _token.user,
+                        (resource_id, system_resource(conn).resource_id)
+                    ).values()
+                    for role in resource.get("roles", tuple()))
+            for privilege in role.privileges)
+        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:
@@ -470,7 +510,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 +548,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 +746,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/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")
diff --git a/gn_auth/auth/errors.py b/gn_auth/auth/errors.py
index 77b73aa..c499e86 100644
--- a/gn_auth/auth/errors.py
+++ b/gn_auth/auth/errors.py
@@ -6,7 +6,7 @@ class AuthorisationError(Exception):
 
     All exceptions in this package should inherit from this class.
     """
-    error_code: int = 400
+    error_code: int = 401
 
 class ForbiddenAccess(AuthorisationError):
     """Raised for forbidden access."""
diff --git a/gn_auth/errors/authlib.py b/gn_auth/errors/authlib.py
index 09862e3..c85b67c 100644
--- a/gn_auth/errors/authlib.py
+++ b/gn_auth/errors/authlib.py
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
 
 def __description__(body):
     """Improve description for errors in authlib.oauth2.rfc6749.errors"""
-    _desc = body["error_description"]
+    _desc = body.get("error_description", body["error"])
     match body["error"]:
         case "missing_authorization":
             return (