about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.guix/modules/gn-auth.scm2
-rw-r--r--.pylintrc13
-rw-r--r--README.md2
-rw-r--r--gn_auth/__init__.py40
-rw-r--r--gn_auth/auth/authentication/oauth2/endpoints/introspection.py2
-rw-r--r--gn_auth/auth/authentication/oauth2/endpoints/revocation.py2
-rw-r--r--gn_auth/auth/authentication/oauth2/endpoints/utilities.py6
-rw-r--r--gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py12
-rw-r--r--gn_auth/auth/authentication/oauth2/models/oauth2client.py17
-rw-r--r--gn_auth/auth/authentication/oauth2/resource_server.py6
-rw-r--r--gn_auth/auth/authentication/oauth2/views.py7
-rw-r--r--gn_auth/auth/authentication/users.py4
-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.py276
-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.py130
-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/models.py11
-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/models.py38
-rw-r--r--gn_auth/auth/authorisation/users/views.py22
-rw-r--r--gn_auth/auth/db/sqlite3.py61
-rw-r--r--gn_auth/auth/errors.py2
-rw-r--r--gn_auth/debug.py22
-rw-r--r--gn_auth/errors/authlib.py2
-rw-r--r--gn_auth/migrations/__init__.py (renamed from gn_auth/migrations.py)3
-rw-r--r--gn_auth/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py (renamed from migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py)0
-rw-r--r--gn_auth/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py (renamed from migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py (renamed from migrations/auth/20221108_01_CoxYh-create-the-groups-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221108_02_wxTr9-create-privileges-table.py (renamed from migrations/auth/20221108_02_wxTr9-create-privileges-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py (renamed from migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py (renamed from migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py (renamed from migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py)0
-rw-r--r--gn_auth/migrations/auth/20221110_01_WtZ1I-create-resources-table.py (renamed from migrations/auth/20221110_01_WtZ1I-create-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221110_05_BaNtL-create-roles-table.py (renamed from migrations/auth/20221110_05_BaNtL-create-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py (renamed from migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py (renamed from migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py (renamed from migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py (renamed from migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py)0
-rw-r--r--gn_auth/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py (renamed from migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py (renamed from migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py)0
-rw-r--r--gn_auth/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py (renamed from migrations/auth/20221114_03_PtWjc-create-group-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py (renamed from migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py)0
-rw-r--r--gn_auth/migrations/auth/20221114_05_hQun6-create-user-roles-table.py (renamed from migrations/auth/20221114_05_hQun6-create-user-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py (renamed from migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py)0
-rw-r--r--gn_auth/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py (renamed from migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py)0
-rw-r--r--gn_auth/migrations/auth/20221117_02_fmuZh-create-group-users-table.py (renamed from migrations/auth/20221117_02_fmuZh-create-group-users-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py (renamed from migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py (renamed from migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py (renamed from migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py (renamed from migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py)0
-rw-r--r--gn_auth/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py (renamed from migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py (renamed from migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py)0
-rw-r--r--gn_auth/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py (renamed from migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py)0
-rw-r--r--gn_auth/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py (renamed from migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py (renamed from migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py)0
-rw-r--r--gn_auth/migrations/auth/20230210_02_lDK14-create-system-admin-role.py (renamed from migrations/auth/20230210_02_lDK14-create-system-admin-role.py)0
-rw-r--r--gn_auth/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py (renamed from migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py)0
-rw-r--r--gn_auth/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py (renamed from migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py)0
-rw-r--r--gn_auth/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py (renamed from migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py (renamed from migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py (renamed from migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py (renamed from migrations/auth/20230404_02_la33P-create-genotype-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py (renamed from migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py (renamed from migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py (renamed from migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py (renamed from migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py)0
-rw-r--r--gn_auth/migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py (renamed from migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py (renamed from migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230912_01_BxrhE-add-system-resource.py (renamed from migrations/auth/20230912_01_BxrhE-add-system-resource.py)0
-rw-r--r--gn_auth/migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py (renamed from migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py)0
-rw-r--r--gn_auth/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py (renamed from migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py)0
-rw-r--r--gn_auth/migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py (renamed from migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py)0
-rw-r--r--gn_auth/migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py (renamed from migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py)0
-rw-r--r--gn_auth/migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py (renamed from migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py)0
-rw-r--r--gn_auth/migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py (renamed from migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py)0
-rw-r--r--gn_auth/migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py (renamed from migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py)0
-rw-r--r--gn_auth/migrations/auth/20240606_02_ubZri-create-resource-roles-table.py (renamed from migrations/auth/20240606_02_ubZri-create-resource-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py (renamed from migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py)0
-rw-r--r--gn_auth/migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py (renamed from migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py)0
-rw-r--r--gn_auth/migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py (renamed from migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py)0
-rw-r--r--gn_auth/migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py (renamed from migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py)0
-rw-r--r--gn_auth/migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py (renamed from migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py)0
-rw-r--r--gn_auth/migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py (renamed from migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py)0
-rw-r--r--gn_auth/migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py (renamed from migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py)0
-rw-r--r--gn_auth/migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py (renamed from migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py)0
-rw-r--r--gn_auth/migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py (renamed from migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py)0
-rw-r--r--gn_auth/migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py (renamed from migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py)0
-rw-r--r--gn_auth/migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py (renamed from migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py)0
-rw-r--r--gn_auth/migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py (renamed from migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py)0
-rw-r--r--gn_auth/migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py (renamed from migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py)0
-rw-r--r--gn_auth/migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py (renamed from migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py)0
-rw-r--r--gn_auth/migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py (renamed from migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py)0
-rw-r--r--gn_auth/migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py (renamed from migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py)0
-rw-r--r--gn_auth/migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py (renamed from migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py)0
-rw-r--r--gn_auth/migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py (renamed from migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py)0
-rw-r--r--gn_auth/migrations/auth/20260331_01_FV1sL-add-privileges-to-role-systemwide-data-curator.py69
-rw-r--r--gn_auth/migrations/auth/20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table.py185
-rw-r--r--gn_auth/migrations/auth/20260428_01_Tak6O-new-privilege-system-system-wide-data-view.py19
-rw-r--r--gn_auth/migrations/auth/20260428_02_L6zIV-add-privileges-to-batch-editors-role.py62
-rw-r--r--gn_auth/migrations/auth/__init__.py (renamed from migrations/auth/__init__.py)0
-rw-r--r--gn_auth/scripts/__init__.py1
-rw-r--r--gn_auth/scripts/assign_data_to_default_admin.py (renamed from scripts/assign_data_to_default_admin.py)0
-rw-r--r--gn_auth/scripts/batch_assign_data_to_default_admin.py (renamed from scripts/batch_assign_data_to_default_admin.py)3
-rw-r--r--gn_auth/scripts/link_inbredsets.py (renamed from scripts/link_inbredsets.py)2
-rw-r--r--gn_auth/scripts/register_sys_admin.py (renamed from scripts/register_sys_admin.py)0
-rw-r--r--gn_auth/scripts/search_phenotypes.py (renamed from scripts/search_phenotypes.py)0
-rw-r--r--gn_auth/scripts/worker.py (renamed from scripts/worker.py)0
-rw-r--r--gn_auth/settings.py4
-rw-r--r--gn_auth/wsgi.py377
-rw-r--r--migrations/__init__.py1
-rw-r--r--pyproject.toml71
-rw-r--r--scripts/__init__.py0
-rw-r--r--setup.cfg4
-rwxr-xr-xsetup.py48
-rw-r--r--setup_commands/__init__.py3
-rw-r--r--setup_commands/run_tests.py40
-rw-r--r--tests/unit/auth/fixtures/group_fixtures.py21
-rw-r--r--tests/unit/auth/fixtures/resource_fixtures.py48
-rw-r--r--tests/unit/auth/fixtures/role_fixtures.py2
-rw-r--r--tests/unit/auth/fixtures/user_fixtures.py21
-rw-r--r--tests/unit/auth/test_groups.py2
-rw-r--r--tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py2
-rw-r--r--tests/unit/auth/test_resources.py18
130 files changed, 1538 insertions, 537 deletions
diff --git a/.guix/modules/gn-auth.scm b/.guix/modules/gn-auth.scm
index 0d9cbc9..190f695 100644
--- a/.guix/modules/gn-auth.scm
+++ b/.guix/modules/gn-auth.scm
@@ -34,7 +34,7 @@
         #~(modify-phases #$phases
             (add-before 'build 'pylint
               (lambda _
-                (invoke "pylint" "setup.py" "tests" "gn_auth" "scripts")))
+                (invoke "pylint" "tests" "gn_auth")))
             (add-after 'pylint 'mypy
               (lambda _
                 (invoke "mypy" ".")))))))
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 0b11d24..0000000
--- a/.pylintrc
+++ /dev/null
@@ -1,13 +0,0 @@
-[SIMILARITIES]
-
-ignore-imports=yes
-
-[MESSAGES CONTROL]
-
-disable=
-	fixme,
-	duplicate-code,
-        no-else-return
-
-load-plugins=
-	pylint.extensions.no_self_use
\ No newline at end of file
diff --git a/README.md b/README.md
index 963b5c5..f6c5f04 100644
--- a/README.md
+++ b/README.md
@@ -268,7 +268,7 @@ The checks we do are
 ### Linting
 
 ```bash
-pylint *py tests gn_auth scripts
+pylint tests gn_auth
 ```
 
 ### Type-Checking
diff --git a/gn_auth/__init__.py b/gn_auth/__init__.py
index d6591e5..d03c9ef 100644
--- a/gn_auth/__init__.py
+++ b/gn_auth/__init__.py
@@ -61,33 +61,24 @@ def load_secrets_conf(app: Flask) -> None:
         app.config.from_pyfile(secretsfile)
 
 
-def dev_loggers(appl: Flask) -> None:
+def dev_loggers(appl: Flask) -> logging.Logger:
     """Setup the logging handlers."""
     stderr_handler = logging.StreamHandler(stream=sys.stderr)
     appl.logger.addHandler(stderr_handler)
+    appl.logger.setLevel(appl.config["LOGLEVEL"])
 
-    root_logger = logging.getLogger()
-    root_logger.addHandler(stderr_handler)
-    root_logger.setLevel(appl.config["LOGLEVEL"])
+    return appl.logger
 
 
-def gunicorn_loggers(appl: Flask) -> None:
+def gunicorn_loggers(appl: Flask) -> logging.Logger:
     """Use gunicorn logging handlers for the application."""
     logger = logging.getLogger("gunicorn.error")
     appl.logger.handlers = logger.handlers
     appl.logger.setLevel(logger.level)
+    return appl.logger
 
 
-_LOGGABLE_MODULES_ = (
-    "gn_auth.errors",
-    "gn_auth.errors.common",
-    "gn_auth.errors.authlib",
-    "gn_auth.errors.http.http_4xx_errors",
-    "gn_auth.errors.http.http_5xx_errors"
-)
-
-
-def setup_logging(appl: Flask) -> None:
+def setup_logging(appl: Flask, loggable_modules: tuple[str, ...] = tuple()) -> None:
     """
     Setup the loggers according to the WSGI server used to run the application.
     """
@@ -96,14 +87,11 @@ def setup_logging(appl: Flask) -> None:
     # https://peps.python.org/pep-3333/#id4
     software, *_version_and_comments = os.environ.get(
         "SERVER_SOFTWARE", "").split('/')
-    if bool(software):
-        gunicorn_loggers(appl)
-    else:
-        dev_loggers(appl)
-
-    loglevel = logging.getLevelName(appl.logger.getEffectiveLevel())
-    for module_logger in _LOGGABLE_MODULES_:
-        logging.getLogger(module_logger).setLevel(loglevel)
+    logger = gunicorn_loggers(appl) if bool(software) else dev_loggers(appl)
+    for _logger in (
+            item for item in logger.manager.loggerDict.values()
+            if isinstance(item, logging.Logger)):
+        _logger.addFilter(lambda record: record.name in loggable_modules)
 
 
 def create_app(config: Optional[dict] = None) -> Flask:
@@ -112,18 +100,18 @@ def create_app(config: Optional[dict] = None) -> Flask:
 
     # ====== Setup configuration ======
     app.config.from_object(settings) # Default settings
-    # Override defaults with startup settings
-    app.config.update(config or {})
     # Override app settings with site-local settings
     if "GN_AUTH_CONF" in os.environ:
         app.config.from_envvar("GN_AUTH_CONF")
 
     override_settings_with_envvars(app)
 
+    # Override defaults with startup settings
+    app.config.update(config or {})
     load_secrets_conf(app)
     # ====== END: Setup configuration ======
 
-    setup_logging(app)
+    setup_logging(app, tuple(app.config.get("LOGGABLE_MODULES", [])))
     check_mandatory_settings(app)
 
     setup_oauth2_server(app)
diff --git a/gn_auth/auth/authentication/oauth2/endpoints/introspection.py b/gn_auth/auth/authentication/oauth2/endpoints/introspection.py
index 200b25d..cebb3be 100644
--- a/gn_auth/auth/authentication/oauth2/endpoints/introspection.py
+++ b/gn_auth/auth/authentication/oauth2/endpoints/introspection.py
@@ -23,7 +23,7 @@ class IntrospectionEndpoint(_IntrospectionEndpoint):
     CLIENT_AUTH_METHODS = ['client_secret_post']
     def query_token(self, token_string: str, token_type_hint: str):
         """Query the token."""
-        return _query_token(self, token_string, token_type_hint)
+        return _query_token(token_string, token_type_hint)
 
     # pylint: disable=[no-self-use]
     def introspect_token(self, token: OAuth2Token) -> dict:
diff --git a/gn_auth/auth/authentication/oauth2/endpoints/revocation.py b/gn_auth/auth/authentication/oauth2/endpoints/revocation.py
index 80922f1..0979694 100644
--- a/gn_auth/auth/authentication/oauth2/endpoints/revocation.py
+++ b/gn_auth/auth/authentication/oauth2/endpoints/revocation.py
@@ -15,7 +15,7 @@ class RevocationEndpoint(_RevocationEndpoint):
     CLIENT_AUTH_METHODS = ['client_secret_post']
     def query_token(self, token_string: str, token_type_hint: str):
         """Query the token."""
-        return _query_token(self, token_string, token_type_hint)
+        return _query_token(token_string, token_type_hint)
 
     def revoke_token(self, token: OAuth2Token, request):
         """Revoke token `token`."""
diff --git a/gn_auth/auth/authentication/oauth2/endpoints/utilities.py b/gn_auth/auth/authentication/oauth2/endpoints/utilities.py
index 08b2a3b..490c141 100644
--- a/gn_auth/auth/authentication/oauth2/endpoints/utilities.py
+++ b/gn_auth/auth/authentication/oauth2/endpoints/utilities.py
@@ -1,5 +1,5 @@
 """endpoint utilities"""
-from typing import Any, Optional
+from typing import Optional
 
 from flask import current_app
 from pymonad.maybe import Nothing
@@ -8,9 +8,7 @@ from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.oauth2.models.oauth2token import (
     OAuth2Token, token_by_access_token, token_by_refresh_token)
 
-def query_token(# pylint: disable=[unused-argument]
-        endpoint_object: Any, token_str: str, token_type_hint) -> Optional[
-            OAuth2Token]:
+def query_token(token_str: str, token_type_hint) -> Optional[OAuth2Token]:
     """Retrieve the token from the database."""
     def __identity__(val):
         """Identity function."""
diff --git a/gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py b/gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py
index c802091..63f979c 100644
--- a/gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py
+++ b/gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py
@@ -1,9 +1,8 @@
 """JWT as Authorisation Grant"""
 import uuid
 import time
-
+import logging
 from typing import Optional
-from flask import current_app as app
 
 from authlib.jose import jwt
 from authlib.common.encoding import to_native
@@ -12,12 +11,17 @@ from authlib.oauth2.rfc7523.jwt_bearer import JWTBearerGrant as _JWTBearerGrant
 from authlib.oauth2.rfc7523.token import (
     JWTBearerTokenGenerator as _JWTBearerTokenGenerator)
 
-from gn_auth.debug import __pk__
+from gn_libs.debug import make_peeker
+
 from gn_auth.auth.db.sqlite3 import with_db_connection
 from gn_auth.auth.authentication.users import User, user_by_id
 from gn_auth.auth.authentication.oauth2.models.oauth2client import OAuth2Client
 
 
+logger = logging.getLogger(__name__)
+__pk__ = make_peeker(logger)
+
+
 class JWTBearerTokenGenerator(_JWTBearerTokenGenerator):
     """
     A JSON Web Token formatted bearer token generator for jwt-bearer grant type.
@@ -149,6 +153,6 @@ class JWTBearerGrant(_JWTBearerGrant):
             include_refresh_token=self.request.client.check_grant_type(
                 "refresh_token")
         )
-        app.logger.debug('Issue token %r to %r', token, self.request.client)
+        logger.debug('Issue token %r to %r', token, self.request.client)
         self.save_token(token)
         return 200, token, self.TOKEN_RESPONSE_HEADER
diff --git a/gn_auth/auth/authentication/oauth2/models/oauth2client.py b/gn_auth/auth/authentication/oauth2/models/oauth2client.py
index fe12ff9..dfe5d79 100644
--- a/gn_auth/auth/authentication/oauth2/models/oauth2client.py
+++ b/gn_auth/auth/authentication/oauth2/models/oauth2client.py
@@ -1,5 +1,6 @@
 """OAuth2 Client model."""
 import json
+import logging
 import datetime
 from uuid import UUID
 from urllib.parse import urlparse
@@ -8,13 +9,12 @@ from dataclasses import asdict, dataclass
 from typing import Any, Sequence, Optional
 
 import requests
-from flask import current_app as app
 from requests.exceptions import JSONDecodeError
 from authlib.jose import KeySet, JsonWebKey
 from authlib.oauth2.rfc6749 import ClientMixin
 from pymonad.maybe import Just, Maybe, Nothing
+from gn_libs.debug import make_peeker
 
-from gn_auth.debug import __pk__
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.errors import NotFoundError
 from gn_auth.auth.authentication.users import (User,
@@ -23,6 +23,10 @@ from gn_auth.auth.authentication.users import (User,
                                                same_password)
 
 
+logger = logging.getLogger(__name__)
+__pk__ = make_peeker(logger)
+
+
 @dataclass(frozen=True)
 class OAuth2Client(ClientMixin):
     """
@@ -66,7 +70,7 @@ class OAuth2Client(ClientMixin):
         jwksuri = self.client_metadata.get("public-jwks-uri")
         __pk__(f"PUBLIC JWKs link for client {self.client_id}", jwksuri)
         if not bool(jwksuri):
-            app.logger.debug("No Public JWKs URI set for client!")
+            logger.debug("No Public JWKs URI set for client!")
             return KeySet([])
         try:
             ## IMPORTANT: This can cause a deadlock if the client is working in
@@ -78,13 +82,12 @@ class OAuth2Client(ClientMixin):
                                    timeout=300,
                                    allow_redirects=True).json()["jwks"]])
         except requests.ConnectionError as _connerr:
-            app.logger.debug(
+            logger.debug(
                 "Could not connect to provided URI: %s", jwksuri, exc_info=True)
         except JSONDecodeError as _jsonerr:
-            app.logger.debug(
-                "Could not convert response to JSON", exc_info=True)
+            logger.debug("Could not convert response to JSON", exc_info=True)
         except Exception as _exc:# pylint: disable=[broad-except]
-            app.logger.debug(
+            logger.debug(
                 "Error retrieving the JWKs for the client.", exc_info=True)
         return KeySet([])
 
diff --git a/gn_auth/auth/authentication/oauth2/resource_server.py b/gn_auth/auth/authentication/oauth2/resource_server.py
index 8ecf923..edab02c 100644
--- a/gn_auth/auth/authentication/oauth2/resource_server.py
+++ b/gn_auth/auth/authentication/oauth2/resource_server.py
@@ -1,4 +1,5 @@
 """Protect the resources endpoints"""
+import logging
 from datetime import datetime, timezone, timedelta
 
 from flask import current_app as app
@@ -16,6 +17,9 @@ from gn_auth.auth.authentication.oauth2.models.jwt_bearer_token import (
 from gn_auth.auth.authentication.oauth2.models.oauth2token import (
     token_by_access_token)
 
+logger = logging.getLogger(__name__)
+
+
 class BearerTokenValidator(_BearerTokenValidator):
     """Extends `authlib.oauth2.rfc6750.BearerTokenValidator`"""
     def authenticate_token(self, token_string: str):
@@ -66,7 +70,7 @@ class JWTBearerTokenValidator(_JWTBearerTokenValidator):
                 claims.validate()
                 return claims
             except JoseError as error:
-                app.logger.debug('Authenticate token failed. %r', error)
+                logger.debug('Authenticate token failed. %r', error)
 
         return None
 
diff --git a/gn_auth/auth/authentication/oauth2/views.py b/gn_auth/auth/authentication/oauth2/views.py
index 6c3de51..8cc123f 100644
--- a/gn_auth/auth/authentication/oauth2/views.py
+++ b/gn_auth/auth/authentication/oauth2/views.py
@@ -1,5 +1,6 @@
 """Endpoints for the oauth2 server"""
 import uuid
+import logging
 import traceback
 from urllib.parse import urlparse
 
@@ -27,8 +28,10 @@ from .endpoints.revocation import RevocationEndpoint
 from .endpoints.introspection import IntrospectionEndpoint
 
 
+logger = logging.getLogger(__name__)
 auth = Blueprint("auth", __name__)
 
+
 @auth.route("/delete-client/<uuid:client_id>", methods=["GET", "POST"])
 def delete_client(client_id: uuid.UUID):
     """Delete an OAuth2 client."""
@@ -91,11 +94,11 @@ def authorise():
                 flash(email_passwd_msg, "alert alert-danger")
                 return redirect_response # type: ignore[return-value]
             except EmailNotValidError as _enve:
-                app.logger.debug(traceback.format_exc())
+                logger.debug(traceback.format_exc())
                 flash(email_passwd_msg, "alert alert-danger")
                 return redirect_response # type: ignore[return-value]
             except NotFoundError as _nfe:
-                app.logger.debug(traceback.format_exc())
+                logger.debug(traceback.format_exc())
                 flash(email_passwd_msg, "alert alert-danger")
                 return redirect_response # type: ignore[return-value]
 
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/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..228d95f 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,141 @@ 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:
-        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
-                }
-        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))
-            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
+    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
         }
-        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):
+        if (len(_all_resources.keys()) == 0 and
+                len(_dset_traits.get("Temp", tuple())) == 0):
+            raise NotFoundError(
+                "No resource(s) found for specified trait(s). Do(es) the "
+                "trait(s) actually exist?")
+
+        # Handle Temp traits specially - they should be public/anonymous resources
+        if len(_dset_traits.get("Temp", tuple())) > 0:
+            # Create a synthetic public resource for Temp traits
+            # Use a predictable ID to identify synthetic temp resources
+            temp_resource_id = "gn-auth-temp-traits"
+            _all_resources[temp_resource_id] = {
+                "resource_id": temp_resource_id,
+                "resource_data": tuple(f"{dset}::{trait}" for dset, trait in _dset_traits["Temp"])
+            }
+
+        _resource_ids = tuple(_all_resources.keys())
+
+
+        def __explode_resource_data__(trait_fullname):
+            _dset, _trt = trait_fullname.split("::")
             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)))
+                "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))
+        _privileges_by_resource: dict[str, tuple[str, ...]] = {}
+
+        # Separate synthetic temp resources from real resources
+        temp_resource_id = "gn-auth-temp-traits"
+        real_resource_ids = tuple(rid for rid in _resource_ids if rid != temp_resource_id)
+
+        # Query privileges only for real resources
+        if len(real_resource_ids) > 0:
+            real_paramstr = ", ".join(["?"] * len(real_resource_ids))
+            try:
+                with require_oauth.acquire("profile group resource") as _token:
+                    user = _token.user
+                    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 ({real_paramstr})",
+                        (str(user.user_id),) + real_resource_ids
+                    )
+                    _privileges_by_resource = 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":
+                    cursor.execute(
+                        "SELECT rsc.resource_id "
+                        "FROM resources AS rsc "
+                        "WHERE rsc.public = '1' "
+                        f"AND rsc.resource_id IN ({real_paramstr}) ",
+                        real_resource_ids)
+                    _privileges_by_resource = {
+                        row["resource_id"]: ('group:resource:view-resource',)
+                        for row in cursor.fetchall()
+                    }
+                else:
+                    raise exc from None
+
+        # Temp resources are always publicly viewable
+        if temp_resource_id in _resource_ids:
+            _privileges_by_resource[temp_resource_id] = ('group:resource:view-resource',)
+
+        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 +241,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 b8eaacb..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 .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, ...]:
@@ -245,8 +333,7 @@ def link_data_to_resource(
     """Link data to resource."""
     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,8 +348,7 @@ def unlink_data_from_resource(
     """Unlink data from resource."""
     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))
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/models.py b/gn_auth/auth/authorisation/users/admin/models.py
index 3d68932..0594864 100644
--- a/gn_auth/auth/authorisation/users/admin/models.py
+++ b/gn_auth/auth/authorisation/users/admin/models.py
@@ -4,6 +4,7 @@ import warnings
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.users import User
 from gn_auth.auth.authorisation.roles.models import Role, db_rows_to_roles
+from gn_auth.auth.authorisation.resources.system.models import system_resource
 
 
 def sysadmin_role(conn: db.DbConnection) -> Role:
@@ -28,14 +29,14 @@ def grant_sysadmin_role(cursor: db.DbCursor, user: User) -> User:
     cursor.execute(
             "SELECT * FROM roles WHERE role_name='system-administrator'")
     admin_role = cursor.fetchone()
-    cursor.execute("SELECT resources.resource_id FROM resources")
-    cursor.executemany(
+    sysresource = system_resource(cursor)
+    cursor.execute(
         "INSERT INTO user_roles VALUES (:user_id, :role_id, :resource_id)",
-        tuple({
+        {
             "user_id": str(user.user_id),
             "role_id": admin_role["role_id"],
-            "resource_id": resource_id
-        } for resource_id in cursor.fetchall()))
+            "resource_id": str(sysresource.resource_id)
+        })
     return user
 
 
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/models.py b/gn_auth/auth/authorisation/users/models.py
index d30bfd0..ab7a980 100644
--- a/gn_auth/auth/authorisation/users/models.py
+++ b/gn_auth/auth/authorisation/users/models.py
@@ -1,5 +1,6 @@
 """Functions for acting on users."""
 import uuid
+import warnings
 from functools import reduce
 from datetime import datetime, timedelta
 
@@ -128,3 +129,40 @@ def user_resource_roles(conn: db.DbConnection, user: User) -> dict[uuid.UUID, tu
             (str(user.user_id),))
         return __build_resource_roles__(
             (dict(row) for row in cursor.fetchall()))
+
+
+def delete_users_by_id(
+        conn: db.DbConnection,
+        user_ids: tuple[uuid.UUID, ...]
+) -> int:
+    """Delete users unconditionally by ID, removing all dependent data.
+
+    Unlike the HTTP endpoint, this bypasses all policy checks — users are
+    deleted regardless of their roles or group memberships. Returns the
+    number of users removed from the users table.
+    """
+    warnings.warn(
+        (f"Running dangerous function `{__name__}.delete_users_by_id`. "
+         "Do ensure that is what you actually want."),
+        category=RuntimeWarning)
+    if not user_ids:
+        return 0
+    _ids = tuple(str(uid) for uid in user_ids)
+    _paramstr = ", ".join(["?"] * len(_ids))
+    _dependent_tables = (
+        ("authorisation_code", "user_id"),
+        ("forgot_password_tokens", "user_id"),
+        ("group_join_requests", "requester_id"),
+        ("jwt_refresh_tokens", "user_id"),
+        ("oauth2_tokens", "user_id"),
+        ("user_credentials", "user_id"),
+        ("user_roles", "user_id"),
+        ("user_verification_codes", "user_id"),
+    )
+    with db.cursor(conn) as cursor:
+        for table, col in _dependent_tables:
+            cursor.execute(
+                f"DELETE FROM {table} WHERE {col} IN ({_paramstr})", _ids)
+        cursor.execute(
+            f"DELETE FROM users WHERE user_id IN ({_paramstr})", _ids)
+        return cursor.rowcount
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/db/sqlite3.py b/gn_auth/auth/db/sqlite3.py
index 12a46c7..5f54752 100644
--- a/gn_auth/auth/db/sqlite3.py
+++ b/gn_auth/auth/db/sqlite3.py
@@ -1,63 +1,28 @@
 """Handle connection to auth database."""
-import sqlite3
-import logging
-import contextlib
-from typing import Any, Protocol, Callable, Iterator
-
-import traceback
+import warnings
+from typing import Any, Callable
 
 from flask import current_app
 
-from .protocols import DbCursor
-
-class DbConnection(Protocol):
-    """Type annotation for a generic database connection object."""
-    def cursor(self) -> Any:
-        """A cursor object"""
-
-    def commit(self) -> Any:
-        """Commit the transaction."""
-
-    def rollback(self) -> Any:
-        """Rollback the transaction."""
+from gn_libs.sqlite3 import cursor, connection # pylint: disable=[unused-import]
+from gn_libs.protocols import DbCursor, DbConnection # pylint: disable=[unused-import]
 
-@contextlib.contextmanager
-def connection(db_path: str, row_factory: Callable = sqlite3.Row) -> Iterator[DbConnection]:
-    """Create the connection to the auth database."""
-    logging.debug("SQLite3 DB Path: '%s'.", db_path)
-    conn = sqlite3.connect(db_path)
-    conn.row_factory = row_factory
-    conn.set_trace_callback(logging.debug)
-    conn.execute("PRAGMA foreign_keys = ON")
-    try:
-        yield conn
-    except sqlite3.Error as exc:
-        conn.rollback()
-        logging.debug(traceback.format_exc())
-        raise exc
-    finally:
-        conn.commit()
-        conn.close()
+warnings.warn(
+    f"Module '{__name__}' is deprecated. Use `gn_libs.sqlite3` instead.",
+    category=DeprecationWarning,
+    stacklevel=2)
 
-@contextlib.contextmanager
-def cursor(conn: DbConnection) -> Iterator[DbCursor]:
-    """Get a cursor from the given connection to the auth database."""
-    cur = conn.cursor()
-    try:
-        yield cur
-        conn.commit()
-    except sqlite3.Error as exc:
-        conn.rollback()
-        logging.debug(traceback.format_exc())
-        raise exc
-    finally:
-        cur.close()
 
 def with_db_connection(func: Callable[[DbConnection], Any]) -> Any:
     """
     Takes a function of one argument `func`, whose one argument is a database
     connection.
     """
+    warnings.warn(
+        (f"Function '{__name__}.with_db_connection' is deprecated. "
+         "Use `gn_libs.sqlite3.with_db_connection` instead."),
+        category=DeprecationWarning,
+        stacklevel=2)
     db_uri = current_app.config["AUTH_DB"]
     with connection(db_uri) as conn:
         return func(conn)
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/debug.py b/gn_auth/debug.py
deleted file mode 100644
index 6b7173b..0000000
--- a/gn_auth/debug.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Debug utilities"""
-import logging
-from flask import current_app
-
-__this_module_name__ = __name__
-
-
-# pylint: disable=invalid-name
-def getLogger(name: str):
-    """Return a logger"""
-    return (
-        logging.getLogger(name)
-        if not bool(current_app)
-        else current_app.logger)
-
-def __pk__(*args):
-    """Format log entry"""
-    value = args[-1]
-    title_vals = " => ".join(args[0:-1])
-    logger = getLogger(__this_module_name__)
-    logger.debug("%s: %s", title_vals, value)
-    return value
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 (
diff --git a/gn_auth/migrations.py b/gn_auth/migrations/__init__.py
index 3451e07..6acb058 100644
--- a/gn_auth/migrations.py
+++ b/gn_auth/migrations/__init__.py
@@ -1,4 +1,5 @@
-"""Run the migrations in the app, rather than with yoyo CLI."""
+"""Migrations package: Provides the migrations, and some utility functions to
+help with dealing with migrations."""
 from pathlib import Path
 from typing import Union
 
diff --git a/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py b/gn_auth/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py
index d511f5d..d511f5d 100644
--- a/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py
+++ b/gn_auth/migrations/auth/20221103_01_js9ub-initialise-the-auth-entic-oris-ation-database.py
diff --git a/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py b/gn_auth/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py
index 48bd663..48bd663 100644
--- a/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py
+++ b/gn_auth/migrations/auth/20221103_02_sGrIs-create-user-credentials-table.py
diff --git a/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py b/gn_auth/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py
index 29f92d4..29f92d4 100644
--- a/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py
+++ b/gn_auth/migrations/auth/20221108_01_CoxYh-create-the-groups-table.py
diff --git a/migrations/auth/20221108_02_wxTr9-create-privileges-table.py b/gn_auth/migrations/auth/20221108_02_wxTr9-create-privileges-table.py
index 67720b2..67720b2 100644
--- a/migrations/auth/20221108_02_wxTr9-create-privileges-table.py
+++ b/gn_auth/migrations/auth/20221108_02_wxTr9-create-privileges-table.py
diff --git a/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py b/gn_auth/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py
index ce752ef..ce752ef 100644
--- a/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py
+++ b/gn_auth/migrations/auth/20221108_03_Pbhb1-create-resource-categories-table.py
diff --git a/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py b/gn_auth/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py
index 76ffbef..76ffbef 100644
--- a/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py
+++ b/gn_auth/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py
diff --git a/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py b/gn_auth/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py
index 6c829b1..6c829b1 100644
--- a/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py
+++ b/gn_auth/migrations/auth/20221109_01_HbD5F-add-resource-meta-field-to-resource-categories-field.py
diff --git a/migrations/auth/20221110_01_WtZ1I-create-resources-table.py b/gn_auth/migrations/auth/20221110_01_WtZ1I-create-resources-table.py
index abc8895..abc8895 100644
--- a/migrations/auth/20221110_01_WtZ1I-create-resources-table.py
+++ b/gn_auth/migrations/auth/20221110_01_WtZ1I-create-resources-table.py
diff --git a/migrations/auth/20221110_05_BaNtL-create-roles-table.py b/gn_auth/migrations/auth/20221110_05_BaNtL-create-roles-table.py
index 51e19e8..51e19e8 100644
--- a/migrations/auth/20221110_05_BaNtL-create-roles-table.py
+++ b/gn_auth/migrations/auth/20221110_05_BaNtL-create-roles-table.py
diff --git a/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py b/gn_auth/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py
index 2b55c2b..2b55c2b 100644
--- a/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py
+++ b/gn_auth/migrations/auth/20221110_06_Pq2kT-create-generic-roles-table.py
diff --git a/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py b/gn_auth/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py
index 0d0eeb9..0d0eeb9 100644
--- a/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py
+++ b/gn_auth/migrations/auth/20221110_07_7WGa1-create-role-privileges-table.py
diff --git a/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py b/gn_auth/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py
index 077182b..077182b 100644
--- a/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py
+++ b/gn_auth/migrations/auth/20221110_08_23psB-add-privilege-category-and-privilege-description-columns-to-privileges-table.py
diff --git a/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py b/gn_auth/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py
index 072f226..072f226 100644
--- a/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py
+++ b/gn_auth/migrations/auth/20221113_01_7M0hv-enumerate-initial-privileges.py
diff --git a/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py b/gn_auth/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py
index 2048f4a..2048f4a 100644
--- a/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py
+++ b/gn_auth/migrations/auth/20221114_01_n8gsF-create-generic-role-privileges-table.py
diff --git a/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py b/gn_auth/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py
index 6bd101b..6bd101b 100644
--- a/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py
+++ b/gn_auth/migrations/auth/20221114_02_DKKjn-drop-generic-role-tables.py
diff --git a/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py b/gn_auth/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py
index a7e7b45..a7e7b45 100644
--- a/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py
+++ b/gn_auth/migrations/auth/20221114_03_PtWjc-create-group-roles-table.py
diff --git a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py b/gn_auth/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
index 386f481..386f481 100644
--- a/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
+++ b/gn_auth/migrations/auth/20221114_04_tLUzB-initialise-basic-roles.py
diff --git a/migrations/auth/20221114_05_hQun6-create-user-roles-table.py b/gn_auth/migrations/auth/20221114_05_hQun6-create-user-roles-table.py
index e0de751..e0de751 100644
--- a/migrations/auth/20221114_05_hQun6-create-user-roles-table.py
+++ b/gn_auth/migrations/auth/20221114_05_hQun6-create-user-roles-table.py
diff --git a/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py b/gn_auth/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py
index 2e4ae28..2e4ae28 100644
--- a/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py
+++ b/gn_auth/migrations/auth/20221116_01_nKUmX-add-privileges-to-group-leader-role.py
diff --git a/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py b/gn_auth/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py
index a4d7806..a4d7806 100644
--- a/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py
+++ b/gn_auth/migrations/auth/20221117_01_RDlfx-modify-group-roles-add-group-role-id.py
diff --git a/migrations/auth/20221117_02_fmuZh-create-group-users-table.py b/gn_auth/migrations/auth/20221117_02_fmuZh-create-group-users-table.py
index 92885ef..92885ef 100644
--- a/migrations/auth/20221117_02_fmuZh-create-group-users-table.py
+++ b/gn_auth/migrations/auth/20221117_02_fmuZh-create-group-users-table.py
diff --git a/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py b/gn_auth/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py
index 9aa3667..9aa3667 100644
--- a/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py
+++ b/gn_auth/migrations/auth/20221206_01_BbeF9-create-group-user-roles-on-resources-table.py
diff --git a/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py b/gn_auth/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py
index 2238069..2238069 100644
--- a/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py
+++ b/gn_auth/migrations/auth/20221208_01_sSdHz-add-public-column-to-resources-table.py
diff --git a/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py b/gn_auth/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py
index 475be01..475be01 100644
--- a/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py
+++ b/gn_auth/migrations/auth/20221219_01_CI3tN-create-oauth2-clients-table.py
diff --git a/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py b/gn_auth/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py
index 778282b..778282b 100644
--- a/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py
+++ b/gn_auth/migrations/auth/20221219_02_buSEU-create-oauth2-tokens-table.py
diff --git a/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py b/gn_auth/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py
index 1683f87..1683f87 100644
--- a/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py
+++ b/gn_auth/migrations/auth/20221219_03_PcTrb-create-authorisation-code-table.py
diff --git a/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py b/gn_auth/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py
index 7e7fda2..7e7fda2 100644
--- a/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py
+++ b/gn_auth/migrations/auth/20230111_01_Wd6IZ-remove-create-group-privilege-from-group-leader.py
diff --git a/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py b/gn_auth/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py
index 1ef5ab0..1ef5ab0 100644
--- a/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py
+++ b/gn_auth/migrations/auth/20230116_01_KwuJ3-rework-privileges-schema.py
diff --git a/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py b/gn_auth/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py
index ceae5ea..ceae5ea 100644
--- a/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py
+++ b/gn_auth/migrations/auth/20230207_01_r0bkZ-create-group-join-requests-table.py
diff --git a/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py b/gn_auth/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py
index 8b406a6..8b406a6 100644
--- a/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py
+++ b/gn_auth/migrations/auth/20230210_01_8xMa1-system-admin-privileges-for-data-distribution.py
diff --git a/migrations/auth/20230210_02_lDK14-create-system-admin-role.py b/gn_auth/migrations/auth/20230210_02_lDK14-create-system-admin-role.py
index 9b3fc2b..9b3fc2b 100644
--- a/migrations/auth/20230210_02_lDK14-create-system-admin-role.py
+++ b/gn_auth/migrations/auth/20230210_02_lDK14-create-system-admin-role.py
diff --git a/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py b/gn_auth/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py
index 84bbd49..84bbd49 100644
--- a/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py
+++ b/gn_auth/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py
diff --git a/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py b/gn_auth/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py
index 3caad55..3caad55 100644
--- a/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py
+++ b/gn_auth/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py
diff --git a/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py b/gn_auth/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py
index 647325f..647325f 100644
--- a/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py
+++ b/gn_auth/migrations/auth/20230322_01_0dDZR-create-linked-phenotype-data-table.py
diff --git a/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py b/gn_auth/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py
index 7c9e986..7c9e986 100644
--- a/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py
+++ b/gn_auth/migrations/auth/20230322_02_Ll854-create-phenotype-resources-table.py
diff --git a/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py b/gn_auth/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py
index 02e8718..02e8718 100644
--- a/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py
+++ b/gn_auth/migrations/auth/20230404_01_VKxXg-create-linked-genotype-data-table.py
diff --git a/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py b/gn_auth/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py
index 1a865e0..1a865e0 100644
--- a/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py
+++ b/gn_auth/migrations/auth/20230404_02_la33P-create-genotype-resources-table.py
diff --git a/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py b/gn_auth/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py
index db9a6bf..db9a6bf 100644
--- a/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py
+++ b/gn_auth/migrations/auth/20230410_01_8mwaf-create-linked-mrna-data-table.py
diff --git a/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py b/gn_auth/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py
index 2ad1056..2ad1056 100644
--- a/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py
+++ b/gn_auth/migrations/auth/20230410_02_WZqSf-create-mrna-resources-table.py
diff --git a/migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py b/gn_auth/migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py
index 37fcfe7..37fcfe7 100644
--- a/migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py
+++ b/gn_auth/migrations/auth/20230907_01_pjnxz-refactor-add-resource-ownership-table.py
diff --git a/migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py b/gn_auth/migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py
index c4397c9..c4397c9 100644
--- a/migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py
+++ b/gn_auth/migrations/auth/20230907_02_Enicg-refactor-add-system-and-group-resource-categories.py
diff --git a/migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py b/gn_auth/migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py
index 0f491c2..0f491c2 100644
--- a/migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py
+++ b/gn_auth/migrations/auth/20230907_03_BwAmf-refactor-drop-group-id-from-resources-table.py
diff --git a/migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py b/gn_auth/migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py
index a26834a..a26834a 100644
--- a/migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py
+++ b/gn_auth/migrations/auth/20230907_04_3LnrG-refactor-create-group-resources-table.py
diff --git a/migrations/auth/20230912_01_BxrhE-add-system-resource.py b/gn_auth/migrations/auth/20230912_01_BxrhE-add-system-resource.py
index 66c6461..66c6461 100644
--- a/migrations/auth/20230912_01_BxrhE-add-system-resource.py
+++ b/gn_auth/migrations/auth/20230912_01_BxrhE-add-system-resource.py
diff --git a/migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py b/gn_auth/migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py
index 1b3f0b1..1b3f0b1 100644
--- a/migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py
+++ b/gn_auth/migrations/auth/20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table.py
diff --git a/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py b/gn_auth/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py
index 1172034..1172034 100644
--- a/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py
+++ b/gn_auth/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py
diff --git a/migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py b/gn_auth/migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py
index 402e9a5..402e9a5 100644
--- a/migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py
+++ b/gn_auth/migrations/auth/20231002_01_tzxTf-link-inbredsets-to-auth-system.py
diff --git a/migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py b/gn_auth/migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py
index a4238ed..a4238ed 100644
--- a/migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py
+++ b/gn_auth/migrations/auth/20231011_01_CS8NZ-create-new-inbredset-group-owner-role.py
diff --git a/migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py b/gn_auth/migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py
index 049ac6b..049ac6b 100644
--- a/migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py
+++ b/gn_auth/migrations/auth/20240506_01_798tW-create-jwt-refresh-tokens-table.py
diff --git a/migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py b/gn_auth/migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py
index 0cab1c3..0cab1c3 100644
--- a/migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py
+++ b/gn_auth/migrations/auth/20240529_01_ALNWj-update-schema-for-user-verification.py
diff --git a/migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py b/gn_auth/migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py
index a45fd30..a45fd30 100644
--- a/migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py
+++ b/gn_auth/migrations/auth/20240606_01_xQDwL-move-role-manipulation-privileges-from-group-to-resources.py
diff --git a/migrations/auth/20240606_02_ubZri-create-resource-roles-table.py b/gn_auth/migrations/auth/20240606_02_ubZri-create-resource-roles-table.py
index 0695c0e..0695c0e 100644
--- a/migrations/auth/20240606_02_ubZri-create-resource-roles-table.py
+++ b/gn_auth/migrations/auth/20240606_02_ubZri-create-resource-roles-table.py
diff --git a/migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py b/gn_auth/migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py
index 45d689c..45d689c 100644
--- a/migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py
+++ b/gn_auth/migrations/auth/20240606_03_BY7Us-drop-group-roles-table.py
diff --git a/migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py b/gn_auth/migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py
index 44318bd..44318bd 100644
--- a/migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py
+++ b/gn_auth/migrations/auth/20240819_01_p2vXR-create-forgot-password-tokens-table.py
diff --git a/migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py b/gn_auth/migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py
index 5c6e81d..5c6e81d 100644
--- a/migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py
+++ b/gn_auth/migrations/auth/20240924_01_thbvh-hooks-for-edu-domains.py
diff --git a/migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py b/gn_auth/migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py
index d22ad01..d22ad01 100644
--- a/migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py
+++ b/gn_auth/migrations/auth/20250328_01_72EFk-add-admin-ui-privilege-to-system-administrator-role.py
diff --git a/migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py b/gn_auth/migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py
index 73a4880..73a4880 100644
--- a/migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py
+++ b/gn_auth/migrations/auth/20250609_01_LB60X-add-batch-edit-privileges.py
diff --git a/migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py b/gn_auth/migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py
index 3b9e928..3b9e928 100644
--- a/migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py
+++ b/gn_auth/migrations/auth/20250609_01_bj9Pl-add-new-group-data-link-to-group-privilege.py
diff --git a/migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py b/gn_auth/migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py
index 5d9c306..5d9c306 100644
--- a/migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py
+++ b/gn_auth/migrations/auth/20250609_02_9UBPl-assign-group-data-link-to-group-privilege-to-group-leader.py
diff --git a/migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py b/gn_auth/migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py
index 6335152..6335152 100644
--- a/migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py
+++ b/gn_auth/migrations/auth/20250703_01_aDVwP-add-role-management-privileges-to-group-leader-role.py
diff --git a/migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py b/gn_auth/migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py
index f00ab11..f00ab11 100644
--- a/migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py
+++ b/gn_auth/migrations/auth/20250722_01_7Gro7-create-new-system-user-edit-privilege.py
diff --git a/migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py b/gn_auth/migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py
index b956bef..b956bef 100644
--- a/migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py
+++ b/gn_auth/migrations/auth/20250722_02_M8TXv-add-system-user-edit-privilege-to-system-admin-role.py
diff --git a/migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py b/gn_auth/migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py
index be0d022..be0d022 100644
--- a/migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py
+++ b/gn_auth/migrations/auth/20250729_01_CNn2p-create-initial-system-wide-resources-access-privileges.py
diff --git a/migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py b/gn_auth/migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py
index e79ab1c..e79ab1c 100644
--- a/migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py
+++ b/gn_auth/migrations/auth/20250729_02_7ycSm-assign-initial-system-wide-resources-access-privileges-to-sys-admins.py
diff --git a/migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py b/gn_auth/migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py
index e3bdc8f..e3bdc8f 100644
--- a/migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py
+++ b/gn_auth/migrations/auth/20250729_03_oCvvq-grant-role-to-all-resources-to-sys-admin-users.py
diff --git a/migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py b/gn_auth/migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py
index 95a6fbb..95a6fbb 100644
--- a/migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py
+++ b/gn_auth/migrations/auth/20250731_01_Ke1us-add-sysadmin-privileges-for-acting-on-groups-members.py
diff --git a/migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py b/gn_auth/migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py
index 63e807a..63e807a 100644
--- a/migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py
+++ b/gn_auth/migrations/auth/20260206_01_v3f4P-add-role-systemwide-data-curator.py
diff --git a/migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py b/gn_auth/migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py
index d618f14..d618f14 100644
--- a/migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py
+++ b/gn_auth/migrations/auth/20260311_01_TfRlV-add-privilege-for-gn-docs-documentation-editing.py
diff --git a/migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py b/gn_auth/migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py
index e79ef6a..e79ef6a 100644
--- a/migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py
+++ b/gn_auth/migrations/auth/20260311_02_v3EFQ-assign-systemwide-docs-editor-role-to-sysadmins.py
diff --git a/migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py b/gn_auth/migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py
index bdf8a56..bdf8a56 100644
--- a/migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py
+++ b/gn_auth/migrations/auth/20260311_03_vxBCX-restrict-access-to-resources-make-public-feature.py
diff --git a/gn_auth/migrations/auth/20260331_01_FV1sL-add-privileges-to-role-systemwide-data-curator.py b/gn_auth/migrations/auth/20260331_01_FV1sL-add-privileges-to-role-systemwide-data-curator.py
new file mode 100644
index 0000000..22863ae
--- /dev/null
+++ b/gn_auth/migrations/auth/20260331_01_FV1sL-add-privileges-to-role-systemwide-data-curator.py
@@ -0,0 +1,69 @@
+"""
+Add privileges to role systemwide-data-curator
+"""
+import contextlib
+
+from yoyo import step
+
+__depends__ = {'20260311_03_vxBCX-restrict-access-to-resources-make-public-feature'}
+
+
+__new_privileges__ = (
+    ("system:system-wide:inbredset:view-case-attribute",
+     "Enable view of any and all inbredset case attributes system-wide."),
+    ("system:system-wide:inbredset:edit-case-attribute",
+     "Enable edit of any and all inbredset case attributes system-wide."),
+    ("system:system-wide:inbredset:delete-case-attribute",
+     "Enable deletion of any and all inbredset case attributes system-wide."),
+    ("system:system-wide:inbredset:apply-case-attribute-edit",
+     "Enable applying changes to any and all inbredset case attributes system-wide."),
+    ("system:system-wide:inbredset:reject-case-attribute-edit",
+     "Enable rejecting changes to any and all inbredset case attributes system-wide."))
+
+
+def fetch_systemwide_data_curator_role_id(cursor):
+    "Fetch the role's ID."
+    cursor.execute("SELECT role_id FROM roles "
+                       "WHERE role_name='systemwide-data-curator'")
+    return cursor.fetchone()[0]
+
+
+def create_new_privileges(conn):
+    """Create new privileges for the system."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.executemany(
+            "INSERT INTO privileges(privilege_id, privilege_description) "
+            "VALUES (?, ?)",
+            __new_privileges__)
+
+
+def delete_new_privileges(conn):
+    """Delete these new privileges from the system."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.executemany("DELETE FROM privileges WHERE privilege_id=?",
+                           tuple((priv[0],) for priv in __new_privileges__))
+
+
+def assign_new_privileges(conn):
+    """Assign the new privileges to the `systemwide-data-curator` role."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        role_id = fetch_systemwide_data_curator_role_id(cursor)
+        cursor.executemany(
+            "INSERT INTO role_privileges(role_id, privilege_id) VALUES (?, ?)",
+            tuple((role_id, privilege[0]) for privilege in __new_privileges__))
+
+
+def revoke_new_privileges(conn):
+    """Revoke the new privileges from the `systemwide-data-curator` role."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        role_id = fetch_systemwide_data_curator_role_id(cursor)
+        cursor.executemany(
+            "DELETE FROM role_privileges WHERE role_id=? AND privilege_id=?",
+            tuple((role_id, privilege[0]) for privilege in __new_privileges__))
+
+
+
+steps = [
+    step(create_new_privileges, delete_new_privileges),
+    step(assign_new_privileges, revoke_new_privileges)
+]
diff --git a/gn_auth/migrations/auth/20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table.py b/gn_auth/migrations/auth/20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table.py
new file mode 100644
index 0000000..702c418
--- /dev/null
+++ b/gn_auth/migrations/auth/20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table.py
@@ -0,0 +1,185 @@
+"""
+Add user and time tracking to resources table
+"""
+import random
+import contextlib
+from datetime import datetime
+
+from yoyo import step
+
+__depends__ = {'20260331_01_FV1sL-add-privileges-to-role-systemwide-data-curator'}
+
+GN_AUTH_INIT_TIMESTAMP = 1691130509.0
+__admin_id__ = ""
+
+
+def fetch_acentenos_id(conn):
+    """Fetch the default resource creator."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute("SELECT user_id FROM users WHERE email=?",
+                       (("acent" "eno@" "uthsc" "." "edu"),))
+        res = cursor.fetchone()
+        return res[0] if bool(res) else None
+
+
+def fetch_a_sysadmin_id(conn, resources_table):
+    """Fetch one ID out of all system administrator users."""
+    global __admin_id__
+
+    def __fetch__():
+        with contextlib.closing(conn.cursor()) as cursor:
+            cursor.execute(
+                f"SELECT ur.user_id FROM {resources_table} AS rsc "
+                "INNER JOIN user_roles AS ur ON rsc.resource_id=ur.resource_id "
+                "INNER JOIN roles AS r ON ur.role_id=r.role_id "
+                "WHERE resource_name='GeneNetwork System' "
+                "AND r.role_name='system-administrator'"
+            )
+            return tuple(row[0] for row in cursor.fetchall())
+
+    if not bool(__admin_id__):
+        __admins__ = __fetch__()
+        if len(__admins__) > 0:
+            __admin_id__ = random.choice(__admins__)
+
+    return __admin_id__
+
+
+def add_user_and_time_tracking_columns(conn):
+    """Add user and time tracking columns."""
+    conn.execute(
+        """
+            CREATE TABLE resources_new(
+              resource_id TEXT NOT NULL,
+              resource_name TEXT NOT NULL UNIQUE,
+              resource_category_id TEXT NOT NULL,
+              public INTEGER NOT NULL DEFAULT 0 CHECK (public=0 or public=1),
+              created_by TEXT NOT NULL,
+              created_at REAL NOT NULL DEFAULT '1691130509.0',
+              PRIMARY KEY(resource_id),
+              FOREIGN KEY(resource_category_id)
+                REFERENCES resource_categories(resource_category_id)
+                ON UPDATE CASCADE ON DELETE RESTRICT,
+              FOREIGN KEY(created_by)
+                REFERENCES users(user_id) ON UPDATE CASCADE ON DELETE RESTRICT
+            ) WITHOUT ROWID
+            """)
+
+
+def drop_user_and_time_tracking_columns(conn):
+    """Drop user and time tracking columns."""
+    conn.execute("PRAGMA foreign_keys = OFF")
+    conn.execute("DROP TABLE IF EXISTS resources")
+    conn.execute("ALTER TABLE resources_old RENAME TO resources")
+    conn.execute("PRAGMA foreign_key_check")
+    conn.execute("PRAGMA foreign_keys = ON")
+
+
+def update_data_for_new_resources_table(conn):
+    """Add creator and time to original data."""
+    __creator__ = (
+        fetch_acentenos_id(conn) or fetch_a_sysadmin_id(conn, "resources"))
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute("SELECT * FROM resources")
+        cursor.executemany(
+            "INSERT INTO resources_new("
+            "  resource_id,"
+            "  resource_name,"
+            "  resource_category_id,"
+            "  public,"
+            "  created_by,"
+            "  created_at"
+            ") VALUES (?, ?, ?, ?, ?, ?)",
+            tuple(
+                tuple(row) + (__creator__, GN_AUTH_INIT_TIMESTAMP)
+                for row in cursor.fetchall()))
+
+
+def restore_data_for_old_resources_table(conn):
+    """Remove creator and time from data."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute("SELECT * FROM resources")
+        cursor.executemany(
+            "INSERT INTO resources_old("
+            "  resource_id,"
+            "  resource_name,"
+            "  resource_category_id,"
+            "  public"
+            ") VALUES (?, ?, ?, ?)",
+            tuple(tuple(row)[0:4] for row in cursor.fetchall()))
+
+
+def replace_old_table_with_new_table(conn):
+    """Restore old resources table with the new resources table."""
+    conn.execute("PRAGMA foreign_keys = OFF")
+    conn.execute("DROP TABLE resources")
+    conn.execute("ALTER TABLE resources_new RENAME TO resources")
+    conn.execute("PRAGMA foreign_key_check")
+    conn.execute("PRAGMA foreign_keys = ON")
+
+
+def restore_old_table(conn):
+    """Restore old 'resources' table schema."""
+    conn.execute(
+        """
+            CREATE TABLE resources_old(
+              resource_id TEXT NOT NULL,
+              resource_name TEXT NOT NULL UNIQUE,
+              resource_category_id TEXT NOT NULL,
+              public INTEGER NOT NULL DEFAULT 0 CHECK (public=0 or public=1),
+              PRIMARY KEY(resource_id),
+              FOREIGN KEY(resource_category_id)
+                REFERENCES resource_categories(resource_category_id)
+                ON UPDATE CASCADE ON DELETE RESTRICT
+            ) WITHOUT ROWID
+            """)
+
+
+def parse_creator_and_time(cursor, row):
+    __return__ = None
+
+    __name_parts__ = row[1].split("—")
+    if len(__name_parts__) == 4:
+        __email__, __inbredsetname__, __datetimestr__, count = __name_parts__
+        cursor.execute("SELECT user_id FROM users WHERE email=?",
+                       (__email__.strip(),))
+        results = cursor.fetchone()
+        if bool(results):
+            __return__ = {
+                "resource_id": row[0],
+                "creator": results[0],
+                "created": datetime.fromisoformat(__datetimestr__).timestamp()
+            }
+
+    return __return__
+
+
+def update_creators_and_time(conn):
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute("SELECT resource_id, resource_name FROM resources")
+        cursor.executemany(
+            "UPDATE resources SET created_by=:creator, created_at=:created "
+            "WHERE resource_id=:resource_id",
+            tuple(item for item in
+                  (parse_creator_and_time(cursor, row)
+                   for row in cursor.fetchall())
+                  if item is not None))
+        
+
+
+def restore_default_creators_and_time(conn):
+    with contextlib.closing(conn.cursor()) as cursor:
+        __creator__ = (
+            fetch_acentenos_id(conn) or fetch_a_sysadmin_id(conn, "resources"))
+        cursor.execute("UPDATE resources SET created_by=?, created_at=?",
+                       (__creator__, GN_AUTH_INIT_TIMESTAMP))
+
+
+steps = [
+    step(add_user_and_time_tracking_columns,
+         drop_user_and_time_tracking_columns),
+    step(update_data_for_new_resources_table,
+         restore_data_for_old_resources_table),
+    step(replace_old_table_with_new_table, restore_old_table),
+    step(update_creators_and_time, restore_default_creators_and_time)
+]
diff --git a/gn_auth/migrations/auth/20260428_01_Tak6O-new-privilege-system-system-wide-data-view.py b/gn_auth/migrations/auth/20260428_01_Tak6O-new-privilege-system-system-wide-data-view.py
new file mode 100644
index 0000000..2dddc56
--- /dev/null
+++ b/gn_auth/migrations/auth/20260428_01_Tak6O-new-privilege-system-system-wide-data-view.py
@@ -0,0 +1,19 @@
+"""
+New privilege: system:system-wide:data:view
+"""
+
+from yoyo import step
+
+__depends__ = {'20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table'}
+
+steps = [
+    step(
+        """
+        INSERT INTO privileges(privilege_id, privilege_description)
+        VALUES('system:system-wide:data:view',
+        'A user with this privilege can view any data on the entire system.')
+        """,
+        """
+        DELETE FROM privileges WHERE privilege_id='system:system-wide:data:view'
+        """)
+]
diff --git a/gn_auth/migrations/auth/20260428_02_L6zIV-add-privileges-to-batch-editors-role.py b/gn_auth/migrations/auth/20260428_02_L6zIV-add-privileges-to-batch-editors-role.py
new file mode 100644
index 0000000..537bf9b
--- /dev/null
+++ b/gn_auth/migrations/auth/20260428_02_L6zIV-add-privileges-to-batch-editors-role.py
@@ -0,0 +1,62 @@
+"""
+Add privileges to batch-editors role
+"""
+import contextlib
+
+from yoyo import step
+
+__depends__ = {'20260428_01_Tak6O-new-privilege-system-system-wide-data-view'}
+
+
+def fetch_batch_editors_role_id(cursor):
+    """Fetch the ID of the batch-editors role."""
+    cursor.execute("SELECT role_id FROM roles WHERE role_name='Batch Editors'")
+    res = cursor.fetchone()
+    if not bool(res):
+        cursor.execute(
+            "SELECT role_id FROM roles WHERE role_name='batch-editors'")
+        res = cursor.fetchone()
+
+    return res[0] if bool(res) else None
+
+
+def rename_role(conn):
+    """Rename role from 'Batch Editors' to 'batch-editors'."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute(
+            "UPDATE roles SET role_name='batch-editors' WHERE role_id=?",
+            (fetch_batch_editors_role_id(cursor),))
+
+
+def restore_old_role_name(conn):
+    """Rename role from 'batch-editors' to 'Batch Editors'."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute(
+            "UPDATE roles SET role_name='Batch Editors' WHERE role_id=?",
+            (fetch_batch_editors_role_id(cursor),))
+
+
+def add_new_privileges(conn):
+    """Add new privileges to 'batch-editors' role."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        role_id = fetch_batch_editors_role_id(cursor)
+        cursor.executemany(
+            "INSERT INTO role_privileges(role_id, privilege_id) VALUES(?, ?)",
+            tuple((role_id, priv) for priv in (
+                "system:system-wide:data:view",
+                "system:system-wide:data:edit")))
+
+
+def remove_new_privileges(conn):
+    """Remove new privileges from 'batch-editors' role."""
+    with contextlib.closing(conn.cursor()) as cursor:
+        cursor.execute(
+            "DELETE FROM role_privileges WHERE role_id=? AND privilege_id IN "
+            "('system:system-wide:data:view', 'system:system-wide:data:edit')",
+            (fetch_batch_editors_role_id(cursor),))
+
+
+steps = [
+    step(rename_role, restore_old_role_name),
+    step(add_new_privileges, remove_new_privileges)
+]
diff --git a/migrations/auth/__init__.py b/gn_auth/migrations/auth/__init__.py
index 1358c9a..1358c9a 100644
--- a/migrations/auth/__init__.py
+++ b/gn_auth/migrations/auth/__init__.py
diff --git a/gn_auth/scripts/__init__.py b/gn_auth/scripts/__init__.py
new file mode 100644
index 0000000..5be56d8
--- /dev/null
+++ b/gn_auth/scripts/__init__.py
@@ -0,0 +1 @@
+"""These are command-line scripts to be run manually or in the background."""
diff --git a/scripts/assign_data_to_default_admin.py b/gn_auth/scripts/assign_data_to_default_admin.py
index 69fc50c..69fc50c 100644
--- a/scripts/assign_data_to_default_admin.py
+++ b/gn_auth/scripts/assign_data_to_default_admin.py
diff --git a/scripts/batch_assign_data_to_default_admin.py b/gn_auth/scripts/batch_assign_data_to_default_admin.py
index a468019..95d9794 100644
--- a/scripts/batch_assign_data_to_default_admin.py
+++ b/gn_auth/scripts/batch_assign_data_to_default_admin.py
@@ -15,8 +15,7 @@ from gn_auth.auth.db import sqlite3 as authdb
 from gn_auth.auth.authentication.users import User
 from gn_auth.auth.authorisation.resources.groups.models import (
     Group, db_row_to_group)
-
-from scripts.assign_data_to_default_admin import (
+from gn_auth.scripts.assign_data_to_default_admin import (
     default_resources, assign_data_to_resource)
 
 
diff --git a/scripts/link_inbredsets.py b/gn_auth/scripts/link_inbredsets.py
index c78a050..ad743f5 100644
--- a/scripts/link_inbredsets.py
+++ b/gn_auth/scripts/link_inbredsets.py
@@ -10,7 +10,7 @@ from gn_libs import mysqldb as biodb
 
 import gn_auth.auth.db.sqlite3 as authdb
 
-from scripts.assign_data_to_default_admin import (
+from gn_auth.scripts.assign_data_to_default_admin import (
     sys_admins, admin_group, select_sys_admin)
 
 def linked_inbredsets(conn):
diff --git a/scripts/register_sys_admin.py b/gn_auth/scripts/register_sys_admin.py
index 06aa845..06aa845 100644
--- a/scripts/register_sys_admin.py
+++ b/gn_auth/scripts/register_sys_admin.py
diff --git a/scripts/search_phenotypes.py b/gn_auth/scripts/search_phenotypes.py
index eee112d..eee112d 100644
--- a/scripts/search_phenotypes.py
+++ b/gn_auth/scripts/search_phenotypes.py
diff --git a/scripts/worker.py b/gn_auth/scripts/worker.py
index 0a77d41..0a77d41 100644
--- a/scripts/worker.py
+++ b/gn_auth/scripts/worker.py
diff --git a/gn_auth/settings.py b/gn_auth/settings.py
index d59e997..f903553 100644
--- a/gn_auth/settings.py
+++ b/gn_auth/settings.py
@@ -14,7 +14,7 @@ SESSION_EXPIRY_MINUTES = 10
 # Database settings
 SQL_URI = "mysql://webqtlout:webqtlout@localhost/db_webqtl"
 AUTH_DB = f"{os.environ.get('HOME')}/genenetwork/gn3_files/db/auth.db"
-AUTH_MIGRATIONS = "migrations/auth"
+AUTH_MIGRATIONS = "gn_auth/migrations/auth"
 
 # Redis settings
 REDIS_URI = "redis://localhost:6379/0"
@@ -49,3 +49,5 @@ EMAIL_ADDRESS = "no-reply@uthsc.edu"
 
 ## Variable settings for various emails going out to users
 AUTH_EMAILS_EXPIRY_MINUTES = 15
+
+LOGGABLE_MODULES = ["gn_auth"]
diff --git a/gn_auth/wsgi.py b/gn_auth/wsgi.py
index e05ef0d..a5af37e 100644
--- a/gn_auth/wsgi.py
+++ b/gn_auth/wsgi.py
@@ -1,10 +1,13 @@
 """Main entry point for project"""
+import os
+import re
+import secrets
 import sys
 import uuid
 import json
 from math import ceil
 from pathlib import Path
-from datetime import datetime
+from datetime import datetime, timezone
 
 import click
 from yoyo import get_backend, read_migrations
@@ -14,10 +17,16 @@ from gn_auth import create_app
 
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.errors import NotFoundError
-from gn_auth.auth.authentication.users import user_by_id, hash_password
-from gn_auth.auth.authorisation.users.admin.models import make_sys_admin
-
-from scripts import register_sys_admin as rsysadm# type: ignore[import]
+from gn_auth.auth.authentication.users import (
+    user_by_id, hash_password, save_user, set_user_password)
+from gn_auth.auth.authorisation.roles.models import assign_default_roles
+from gn_auth.auth.authorisation.users.admin.models import (
+    make_sys_admin, grant_sysadmin_role)
+from gn_auth.auth.authorisation.users.models import delete_users_by_id
+from gn_auth.auth.authentication.oauth2.models.oauth2client import (
+    OAuth2Client, save_client, delete_client,
+    client as oauth2_client_by_id)
+from gn_auth.scripts import register_sys_admin as rsysadm# type: ignore[import]
 
 
 app = create_app()
@@ -127,6 +136,364 @@ def register_admin():
     """Register the administrator."""
     rsysadm.register_admin(Path(app.config["AUTH_DB"]))
 
+
+_VALID_ROLES_ = ("system-admin", "none")
+
+_TEST_EMAIL_DOMAIN_ = "regression-tests.genenetwork.org"
+
+
+def __normalise_name_for_email__(name: str) -> str:
+    """Lowercase and strip non-alphanumeric characters for use in an email."""
+    return re.sub(r"[^a-z0-9]", "", name.lower())
+
+
+def __create_one_user__(cursor, name: str, email: str, password: str, role: str) -> dict:
+    """Create a single user in the DB and return their credential record."""
+    user = save_user(cursor, email, name, verified=True)
+    set_user_password(cursor, user, password)
+    assign_default_roles(cursor, user)
+    if role == "system-admin":
+        grant_sysadmin_role(cursor, user)
+    return {
+        "user_id": str(user.user_id),
+        "name": user.name,
+        "email": user.email,
+        "password": password,
+        "role": role,
+    }
+
+
+def __parse_user_spec__(spec: str) -> dict:
+    """Parse 'key=value,key=value,...' into a dict."""
+    result = {}
+    for part in spec.split(","):
+        key, _, value = part.partition("=")
+        if key.strip():
+            result[key.strip()] = value.strip()
+    return result
+
+
+def __write_output__(data: dict, output_path) -> None:
+    """Write JSON data to a file with 0600 permissions, or stdout."""
+    text = json.dumps(data, indent=2)
+    if output_path is None:
+        print(text)
+        return
+    fd = os.open(output_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+    with os.fdopen(fd, "w") as outfile:
+        outfile.write(text)
+
+
+@app.cli.command()
+@click.option("--user", "user_specs", multiple=True,
+              help='User spec: "name=...,email=...,password=...,role=..."')
+@click.option("--output", "output_path", type=click.Path(), default=None,
+              help="Write credentials as JSON to this file (default: stdout)")
+def create_users(user_specs, output_path):
+    """Create one or more users with specified credentials and roles.
+
+    Each --user option takes a comma-separated key=value string with the
+    following keys: name, email, password, role.
+
+    Valid roles: system-admin, none.
+    """
+    if not user_specs:
+        print("No users specified.", file=sys.stderr)
+        sys.exit(1)
+
+    records = []
+    with db.connection(app.config["AUTH_DB"]) as conn, db.cursor(conn) as cursor:
+        for spec_str in user_specs:
+            spec = __parse_user_spec__(spec_str)
+            name = spec.get("name", "").strip()
+            email = spec.get("email", "").strip()
+            password = spec.get("password", "").strip()
+            role = spec.get("role", "none").strip()
+
+            if not name:
+                print(f"Missing 'name' in user spec: {spec_str!r}", file=sys.stderr)
+                sys.exit(1)
+            if not email:
+                print(f"Missing 'email' in user spec: {spec_str!r}", file=sys.stderr)
+                sys.exit(1)
+            if not password:
+                print(f"Missing 'password' in user spec: {spec_str!r}", file=sys.stderr)
+                sys.exit(1)
+            if role not in _VALID_ROLES_:
+                print(
+                    f"Invalid role {role!r} in spec: {spec_str!r}. "
+                    f"Valid roles: {_VALID_ROLES_}",
+                    file=sys.stderr)
+                sys.exit(1)
+
+            records.append(
+                __create_one_user__(cursor, name, email, password, role))
+
+    __write_output__({"users": records}, output_path)
+
+
+@app.cli.command()
+@click.option("--user-id", "user_ids", multiple=True, type=click.UUID,
+              help="UUID of a user to delete (repeatable)")
+def delete_users(user_ids):
+    """Delete one or more users by ID, bypassing policy checks.
+
+    Removes users unconditionally regardless of their roles or group
+    memberships. Use with care — intended for test teardown and administration.
+    """
+    if not user_ids:
+        print("No user IDs specified.", file=sys.stderr)
+        sys.exit(1)
+
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        deleted = delete_users_by_id(conn, tuple(user_ids))
+        print(f"Deleted {deleted} user(s).")
+
+
+@app.cli.command()
+@click.option("--session-timestamp", required=True,
+              help="Compact ISO 8601 UTC timestamp (e.g. 20260602T122700Z)")
+@click.option("--user", "user_specs", multiple=True,
+              help='User spec: "name=...,role=..."')
+@click.option("--output", "output_path", required=True, type=click.Path(),
+              help="Write credentials as JSON to this file (0600 permissions)")
+def create_test_users(session_timestamp, user_specs, output_path):
+    """Create ephemeral test users with auto-generated email and password.
+
+    Each --user option takes a comma-separated key=value string with the
+    following keys: name, role.
+
+    Email: <normalised-name><timestamp>@regression-tests.genenetwork.org
+    Password: randomly generated.
+
+    Output is written with 0600 permissions. Valid roles: system-admin, none.
+    """
+    if not user_specs:
+        print("No users specified.", file=sys.stderr)
+        sys.exit(1)
+
+    records = []
+    with db.connection(app.config["AUTH_DB"]) as conn, db.cursor(conn) as cursor:
+        for spec_str in user_specs:
+            spec = __parse_user_spec__(spec_str)
+            name = spec.get("name", "").strip()
+            role = spec.get("role", "none").strip()
+
+            if not name:
+                print(f"Missing 'name' in user spec: {spec_str!r}", file=sys.stderr)
+                sys.exit(1)
+            if role not in _VALID_ROLES_:
+                print(
+                    f"Invalid role {role!r} in spec: {spec_str!r}. "
+                    f"Valid roles: {_VALID_ROLES_}",
+                    file=sys.stderr)
+                sys.exit(1)
+
+            email = (f"{__normalise_name_for_email__(name)}"
+                     f"{session_timestamp}@{_TEST_EMAIL_DOMAIN_}")
+            password = secrets.token_urlsafe(32)
+
+            records.append(
+                __create_one_user__(cursor, name, email, password, role))
+
+    __write_output__(
+        {"session_timestamp": session_timestamp, "users": records},
+        output_path)
+
+
+_DEFAULT_GRANT_TYPES_ = (
+    "password",
+    "authorization_code",
+    "refresh_token",
+    "urn:ietf:params:oauth:grant-type:jwt-bearer",
+)
+
+_DEFAULT_SCOPES_ = (
+    "profile", "group", "role", "resource",
+    "register-client", "user", "masquerade",
+    "migrate-data", "introspect",
+)
+
+
+def __create_one_client__(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        conn,
+        client_name: str,
+        owner_user,
+        redirect_uris: tuple,
+        scopes: tuple = _DEFAULT_SCOPES_,
+        grant_types: tuple = _DEFAULT_GRANT_TYPES_,
+        jwks_uri: str = "",
+) -> dict:
+    """Create a single OAuth2 client and return its credential record."""
+    raw_secret = secrets.token_urlsafe(32)
+    the_client = OAuth2Client(
+        client_id=uuid.uuid4(),
+        client_secret=hash_password(raw_secret),
+        client_id_issued_at=datetime.now(tz=timezone.utc),
+        client_secret_expires_at=datetime.fromtimestamp(0),
+        client_metadata={
+            "client_name": client_name,
+            "token_endpoint_auth_method": [
+                "client_secret_post", "client_secret_basic"],
+            "client_type": "confidential",
+            "grant_types": list(grant_types),
+            "default_redirect_uri": redirect_uris[0] if redirect_uris else "",
+            "redirect_uris": list(redirect_uris),
+            "response_type": ["code", "token"],
+            "scope": list(scopes),
+            "public-jwks-uri": jwks_uri,
+        },
+        user=owner_user)
+    save_client(conn, the_client)
+    return {
+        "client_id": str(the_client.client_id),
+        "client_secret": raw_secret,
+        "client_name": client_name,
+    }
+
+
+@app.cli.command()
+@click.option("--name", "client_name", required=True,
+              help="Human-readable name for the OAuth2 client")
+@click.option("--owner-id", required=True, type=click.UUID,
+              help="UUID of the user who owns this client")
+@click.option("--redirect-uri", "redirect_uris", multiple=True,
+              help="Allowed redirect URI (repeatable)")
+@click.option("--scope", "scopes", multiple=True,
+              default=_DEFAULT_SCOPES_, show_default=False,
+              help="OAuth2 scope (repeatable; defaults to full scope set)")
+@click.option("--grant-type", "grant_types", multiple=True,
+              default=_DEFAULT_GRANT_TYPES_, show_default=False,
+              help="Grant type (repeatable; defaults to all standard types)")
+@click.option("--jwks-uri", default="",
+              help="URI to the client's public JWKS (optional)")
+@click.option("--output", "output_path", type=click.Path(), default=None,
+              help="Write credentials as JSON to this file (default: stdout)")
+def create_oauth2_client(# pylint: disable=[too-many-arguments, too-many-positional-arguments]
+        client_name,
+        owner_id,
+        redirect_uris,
+        scopes,
+        grant_types,
+        jwks_uri,
+        output_path
+):
+    """Create an OAuth2 client with specified parameters.
+
+    Scopes and grant types default to the full standard set if not provided.
+    """
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        try:
+            owner = user_by_id(conn, owner_id)
+        except NotFoundError:
+            print(f"No user found with ID {owner_id}", file=sys.stderr)
+            sys.exit(1)
+        record = __create_one_client__(
+            conn, client_name, owner, redirect_uris, scopes, grant_types,
+            jwks_uri)
+
+    __write_output__({"client": record}, output_path)
+
+
+@app.cli.command()
+@click.option("--session-timestamp", required=True,
+              help="Compact ISO 8601 UTC timestamp (e.g. 20260602T122700Z)")
+@click.option("--users-file", required=True, type=click.Path(exists=True),
+              help="Credentials file produced by create-test-users")
+@click.option("--owner-role", default="system-admin", show_default=True,
+              help="Role of the user in users-file to assign as client owner")
+@click.option("--output", "output_path", required=True, type=click.Path(),
+              help="Write credentials as JSON to this file (0600 permissions)")
+def create_test_oauth2_client(session_timestamp, users_file, owner_role,
+                              output_path):
+    """Create an ephemeral OAuth2 client for a test session.
+
+    Reads the credentials file produced by create-test-users to find the
+    owner. Client name and secret are auto-generated using the session
+    timestamp. Output is written with 0600 permissions.
+    """
+    with open(users_file, encoding="utf8") as f:
+        users_data = json.load(f)
+
+    owner_record = next(
+        (u for u in users_data.get("users", []) if u["role"] == owner_role),
+        None)
+    if owner_record is None:
+        print(
+            f"No user with role {owner_role!r} found in {users_file}",
+            file=sys.stderr)
+        sys.exit(1)
+
+    client_name = f"gn-test-client-{session_timestamp}"
+
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        try:
+            owner = user_by_id(conn, uuid.UUID(owner_record["user_id"]))
+        except NotFoundError:
+            print(
+                f"Owner user {owner_record['user_id']!r} not found in DB",
+                file=sys.stderr)
+            sys.exit(1)
+        record = __create_one_client__(conn, client_name, owner, tuple())
+
+    __write_output__(
+        {"session_timestamp": session_timestamp, "client": record},
+        output_path)
+
+
+@app.cli.command()
+@click.option("--credentials", "credentials_path", required=True,
+              type=click.Path(exists=True),
+              help="Credentials file produced by create-oauth2-client or "
+                   "create-test-oauth2-client")
+def delete_oauth2_client(credentials_path):
+    """Delete an OAuth2 client using a credentials file.
+
+    Reads the client_id from the given credentials file and removes the
+    client and all associated tokens from the database.
+    """
+    with open(credentials_path, encoding="utf8") as f:
+        data = json.load(f)
+
+    client_id_str = data.get("client", {}).get("client_id")
+    if not client_id_str:
+        print("No client_id found in credentials file.", file=sys.stderr)
+        sys.exit(1)
+
+    client_id = uuid.UUID(client_id_str)
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        the_client = oauth2_client_by_id(conn, client_id)
+        if the_client.is_nothing():
+            print(f"No client found with ID {client_id}", file=sys.stderr)
+            sys.exit(1)
+        delete_client(conn, the_client.value)
+        print(f"Deleted OAuth2 client {client_id}.")
+
+
+@app.cli.command()
+@click.option("--credentials", "credentials_path", required=True,
+              type=click.Path(exists=True),
+              help="Credentials file produced by create-test-users")
+def delete_test_users(credentials_path):
+    """Delete ephemeral test users using a credentials file.
+
+    Reads the credentials file produced by create-test-users and deletes
+    all listed users unconditionally, bypassing policy checks. Intended
+    for CI test teardown.
+    """
+    with open(credentials_path, encoding="utf8") as f:
+        data = json.load(f)
+
+    user_ids = tuple(
+        uuid.UUID(u["user_id"]) for u in data.get("users", []))
+    if not user_ids:
+        print("No users found in credentials file.", file=sys.stderr)
+        sys.exit(1)
+
+    with db.connection(app.config["AUTH_DB"]) as conn:
+        deleted = delete_users_by_id(conn, user_ids)
+        print(f"Deleted {deleted} user(s).")
+
 ##### END: CLI Commands #####
 
 if __name__ == '__main__':
diff --git a/migrations/__init__.py b/migrations/__init__.py
deleted file mode 100644
index cedf48d..0000000
--- a/migrations/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Migrations package"""
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..f5f624d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,71 @@
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "gn-auth"
+# version = "1.0.1"
+dynamic = ["version"] # Read from git, or elsewhere
+description = "Authentication/Authorisation server for GeneNetwork Services."
+requires-python = ">= 3.10"
+authors = [
+{name = "Frederick M. Muriithi", email = "fredmanglis@gmail.com"},
+]
+dependencies = [
+"argon2-cffi>= 20.1.0",
+"click",
+"Flask>= 1.1.2",
+"mypy>= 0.790",
+"mypy-extensions>= 0.4.3",
+"mysqlclient>= 2.0.1",
+"pylint>= 2.5.3",
+"pymonad",
+"redis>= 3.5.3",
+"requests>= 2.25.1",
+"flask-cors", # with the `>= 3.0.9` specification, it breaks the build
+"gn-libs @ git+https://git.genenetwork.org/gn-libs"
+]
+maintainers = [
+{name = "Frederick M. Muriithi", email = "fredmanglis@gmail.com"},
+]
+license = "AGPL-3.0"
+readme = {file = "README.md", content-type = "text/markdown"}
+
+[project.urls]
+Homepage = "https://git.genenetwork.org/gn-auth/"
+Repository = "https://git.genenetwork.org/gn-auth/"
+
+[dependency-groups]# PEP 735
+tests = ["pytest"]
+checks = [{include-group = "tests"}, "mypy", "pylint", "vulture"]
+
+[tool.pylint.main]
+ignore = ["tests", "venv"]
+ignore-paths = ["^gn_auth/migrations/auth/.*"]
+ignore-imports = true
+disable = ["fixme", "duplicate-code", "no-else-return"]
+load-plugins = ["pylint.extensions.no_self_use"]
+
+[tool.vulture]
+ignore_decorators = [
+"@admin.before_request",
+"@admin.route",
+"@app.cli.command",
+"@auth.route",
+"@collections.route",
+"@data.route",
+"@genobp.route",
+"@groups.route",
+"@masq.route",
+"@misc.route",
+"@phenobp.route",
+"@phenosbp.route",
+"@popbp.route",
+"@privileges.route",
+"@resources.route",
+"@roles.route",
+"@system.route",
+"@users.route"
+]
+exclude = ["*/tests/unit/*", "*/gn_auth/settings.py", "*/gn_auth/migrations/*"]
+min_confidence = 60
\ No newline at end of file
diff --git a/scripts/__init__.py b/scripts/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/scripts/__init__.py
+++ /dev/null
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 41d118e..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-[aliases]
-run_unit_tests = run_tests --type=unit
-run_integration_tests = run_tests --type=integration
-run_performance_tests = run_tests --type=performance
diff --git a/setup.py b/setup.py
deleted file mode 100755
index c7339e2..0000000
--- a/setup.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-"""Setup script for GeneNetwork Auth package."""
-from setuptools import setup, find_packages
-from setup_commands import RunTests
-
-LONG_DESCRIPTION = """
-gn-auth project is the authentication/authorisation server to be used
-across all GeneNetwork services.
-"""
-
-setup(author="Frederick M. Muriithi",
-      author_email="fredmanglis@gmail.com",
-      description=(
-          "Authentication/Authorisation server for GeneNetwork Services."),
-      install_requires=[
-          "argon2-cffi>=20.1.0",
-          "click",
-          "Flask>=1.1.2",
-          "mypy>=0.790",
-          "mypy-extensions>=0.4.3",
-          "mysqlclient>=2.0.1",
-          "pylint>=2.5.3",
-          "pymonad",
-          "redis>=3.5.3",
-          "requests>=2.25.1",
-          "flask-cors", # with the `>=3.0.9` specification, it breaks the build
-          "gn-libs>=0.0.0"
-      ],
-      include_package_data=True,
-      packages=find_packages(
-          where=".",
-          exclude=(
-              "tests",
-              "tests.*",
-              "setup_commands",
-              "setup_commands.*")),
-      # `package_data` doesn't seem to work. Use MANIFEST.in instead
-      scripts=[],
-      license="AGPLV3",
-      long_description=LONG_DESCRIPTION,
-      long_description_content_type="text/markdown",
-      name="gn-auth",
-      url="https://github.com/genenetwork/gn-auth",
-      version="0.0.1",
-      tests_require=["pytest", "hypothesis"],
-      cmdclass={
-          "run_tests": RunTests  # type: ignore[dict-item]
-      })
diff --git a/setup_commands/__init__.py b/setup_commands/__init__.py
deleted file mode 100644
index 967bb11..0000000
--- a/setup_commands/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Module for custom setup commands."""
-
-from .run_tests import RunTests
diff --git a/setup_commands/run_tests.py b/setup_commands/run_tests.py
deleted file mode 100644
index 1bb5dab..0000000
--- a/setup_commands/run_tests.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import os
-import sys
-from distutils.core import Command
-
-class RunTests(Command):
-    """
-    A custom command to run tests.
-    """
-    description = "Run the tests"
-    test_types = (
-        "all", "unit", "integration", "performance")
-    user_options = [
-        ("type=", None,
-         f"""Specify the type of tests to run.
-         Valid types are {tuple(test_types)}.
-         Default is `all`.""")]
-
-    def __init__(self, dist):
-        """Initialise the command."""
-        super().__init__(dist)
-        self.command = "pytest"
-
-    def initialize_options(self):
-        """Initialise the default values of all the options."""
-        self.type = "all"
-
-    def finalize_options(self):
-        """Set final value of all the options once they are processed."""
-        if self.type not in RunTests.test_types:
-            raise Exception(f"""
-            Invalid test type (self.type) requested!
-            Valid types are
-            {tuple(RunTests.test_types)}""")
-
-        if self.type != "all":
-            self.command = f"pytest -m {self.type}_test"
-    def run(self):
-        """Run the chosen tests"""
-        print(f"Running {self.type} tests")
-        os.system(self.command)
diff --git a/tests/unit/auth/fixtures/group_fixtures.py b/tests/unit/auth/fixtures/group_fixtures.py
index 2e8cd9a..da1c4cd 100644
--- a/tests/unit/auth/fixtures/group_fixtures.py
+++ b/tests/unit/auth/fixtures/group_fixtures.py
@@ -1,5 +1,6 @@
 """Fixtures and utilities for group-related tests"""
 import uuid
+import datetime
 
 import pytest
 
@@ -7,8 +8,12 @@ from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authorisation.resources.groups import Group
 from gn_auth.auth.authorisation.resources import Resource, ResourceCategory
 
+from .user_fixtures import TEST_USERS
 from .resource_fixtures import TEST_RESOURCES
 
+
+_created_ = datetime.datetime.now()
+
 TEST_GROUP_01 = Group(uuid.UUID("9988c21d-f02f-4d45-8966-22c968ac2fbf"),
                       "TheTestGroup", {})
 TEST_GROUP_02 = Group(uuid.UUID("e37d59d7-c05e-4d67-b479-81e627d8d634"),
@@ -24,16 +29,20 @@ GROUPS_AS_RESOURCES = tuple({
     "resource_id": res_id,
     "resource_name": group.group_name,
     "category_id": str(GROUP_CATEGORY.resource_category_id),
-    "public": "0"
+    "public": "0",
+    "created_by": str(TEST_USERS[0].user_id),
+    "created_at": _created_.timestamp()
 } for res_id, group in zip(
     ("38d1807d-105f-44a7-8327-7e2d973b6d8d",
      "89458ef6-e090-4b53-8c2c-59eaf2785f11"),
     TEST_GROUPS))
 GROUP_RESOURCES = tuple(
-    Resource(uuid.UUID(row["resource_id"]),
-             row["resource_name"],
+    Resource(uuid.UUID(row["resource_id"]),# type: ignore[arg-type]
+             row["resource_name"],# type: ignore[arg-type]
              GROUP_CATEGORY,
-             False)
+             False,
+             created_by=TEST_USERS[0],
+             created_at=_created_)
     for row in GROUPS_AS_RESOURCES)
 
 
@@ -46,7 +55,7 @@ def __gtuple__(cursor):
     return tuple(dict(row) for row in cursor.fetchall())
 
 @pytest.fixture(scope="function")
-def fxtr_group(conn_after_auth_migrations):# pylint: disable=[redefined-outer-name]
+def fxtr_group(conn_after_auth_migrations, fxtr_users):# pylint: disable=[redefined-outer-name, unused-argument]
     """Fixture: setup a test group."""
     with db.cursor(conn_after_auth_migrations) as cursor:
         cursor.executemany(
@@ -57,7 +66,7 @@ def fxtr_group(conn_after_auth_migrations):# pylint: disable=[redefined-outer-na
 
         cursor.executemany(
             "INSERT INTO resources "
-            "VALUES(:resource_id, :resource_name, :category_id, :public)",
+            "VALUES(:resource_id, :resource_name, :category_id, :public, :created_by, :created_at)",
             GROUPS_AS_RESOURCES)
 
         cursor.executemany(
diff --git a/tests/unit/auth/fixtures/resource_fixtures.py b/tests/unit/auth/fixtures/resource_fixtures.py
index e06f64e..b570a49 100644
--- a/tests/unit/auth/fixtures/resource_fixtures.py
+++ b/tests/unit/auth/fixtures/resource_fixtures.py
@@ -1,11 +1,15 @@
 """Fixtures and utilities for resource-related tests"""
 import uuid
+import datetime
 
 import pytest
 
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authorisation.resources import Resource, ResourceCategory
 
+from .user_fixtures import TEST_USERS
+
+_created_ = datetime.datetime.now()
 
 SYSTEM_CATEGORY = ResourceCategory(
     uuid.UUID("aa3d787f-af6a-44fa-9b0b-c82d40e54ad2"),
@@ -15,48 +19,74 @@ SYSTEM_RESOURCE = Resource(
     uuid.UUID("0248b289-b277-4eaa-8c94-88a434d14b6e"),
     "GeneNetwork System",
     SYSTEM_CATEGORY,
-    True)
+    True,
+    resource_data=tuple(),
+    created_by=TEST_USERS[4],
+    created_at=_created_)
 
 TEST_RESOURCES = (
     Resource(uuid.UUID("26ad1668-29f5-439d-b905-84d551f85955"),
              "ResourceG01R01",
              ResourceCategory(uuid.UUID("48056f84-a2a6-41ac-8319-0e1e212cba2a"),
                               "genotype", "Genotype Dataset"),
-             True),
+             True,
+             resource_data=tuple(),
+             created_by=TEST_USERS[0],
+             created_at=_created_),
     Resource(uuid.UUID("2130aec0-fefd-434d-92fd-9ca342348b2d"),
              "ResourceG01R02",
              ResourceCategory(uuid.UUID("548d684b-d4d1-46fb-a6d3-51a56b7da1b3"),
                               "phenotype", "Phenotype (Publish) Dataset"),
-             False),
+             False,
+             resource_data=tuple(),
+             created_by=TEST_USERS[0],
+             created_at=_created_),
     Resource(uuid.UUID("e9a1184a-e8b4-49fb-b713-8d9cbeea5b83"),
              "ResourceG01R03",
              ResourceCategory(uuid.UUID("fad071a3-2fc8-40b8-992b-cdefe7dcac79"),
                               "mrna", "mRNA Dataset"),
-             False),
+             False,
+             resource_data=tuple(),
+             created_by=TEST_USERS[0],
+             created_at=_created_),
     Resource(uuid.UUID("14496a1c-c234-49a2-978c-8859ea274054"),
              "ResourceG02R01",
              ResourceCategory(uuid.UUID("48056f84-a2a6-41ac-8319-0e1e212cba2a"),
                               "genotype", "Genotype Dataset"),
-             False),
+             False,
+             resource_data=tuple(),
+             created_by=TEST_USERS[0],
+             created_at=_created_),
     Resource(uuid.UUID("04ad9e09-94ea-4390-8a02-11f92999806b"),
              "ResourceG02R02",
              ResourceCategory(uuid.UUID("fad071a3-2fc8-40b8-992b-cdefe7dcac79"),
                               "mrna", "mRNA Dataset"),
-             True))
+             True,
+             resource_data=tuple(),
+             created_by=TEST_USERS[0],
+             created_at=_created_))
 
 TEST_RESOURCES_PUBLIC = (SYSTEM_RESOURCE, TEST_RESOURCES[0], TEST_RESOURCES[4])
 
 
 @pytest.fixture(scope="function")
-def fxtr_resources(conn_after_auth_migrations):
+def fxtr_resources(conn_after_auth_migrations, fxtr_users):# pylint: disable=[unused-argument]
     """fixture: setup test resources in the database"""
     conn = conn_after_auth_migrations
     with db.cursor(conn) as cursor:
         cursor.executemany(
-            "INSERT INTO resources VALUES (?,?,?,?)",
+            "INSERT INTO resources VALUES (?,?,?,?,?,?)",
             ((str(res.resource_id), res.resource_name,
               str(res.resource_category.resource_category_id),
-              1 if res.public else 0) for res in TEST_RESOURCES))
+              1 if res.public else 0,
+              str(res.created_by.user_id),
+              res.created_at.timestamp()) for res in TEST_RESOURCES))
+        cursor.execute(
+            "UPDATE resources SET created_by=?, created_at=? "
+            "WHERE resource_id=?",
+            (str(SYSTEM_RESOURCE.created_by.user_id),
+             SYSTEM_RESOURCE.created_at.timestamp(),
+             str(SYSTEM_RESOURCE.resource_id)))
 
     yield (conn, TEST_RESOURCES)
 
diff --git a/tests/unit/auth/fixtures/role_fixtures.py b/tests/unit/auth/fixtures/role_fixtures.py
index 63a3fca..24e8e9f 100644
--- a/tests/unit/auth/fixtures/role_fixtures.py
+++ b/tests/unit/auth/fixtures/role_fixtures.py
@@ -108,7 +108,7 @@ def fxtr_resource_roles(fxtr_resources, fxtr_roles):# pylint: disable=[redefined
 
 
 @pytest.fixture(scope="function")
-def fxtr_setup_group_leaders(fxtr_users):
+def fxtr_setup_group_leaders(fxtr_users, fxtr_group):# pylint: disable=[unused-argument]
     """Define what roles users have that target resources of type 'Group'."""
     conn, users = fxtr_users
     with db.cursor(conn) as cursor:
diff --git a/tests/unit/auth/fixtures/user_fixtures.py b/tests/unit/auth/fixtures/user_fixtures.py
index 1cf0e20..0872142 100644
--- a/tests/unit/auth/fixtures/user_fixtures.py
+++ b/tests/unit/auth/fixtures/user_fixtures.py
@@ -1,28 +1,35 @@
 """Fixtures and utilities for user-related tests"""
 import uuid
+import datetime
 
 import pytest
 
 from gn_auth.auth.db import sqlite3 as db
 from gn_auth.auth.authentication.users import User, hash_password
 
+_created_ = datetime.datetime.now()
+
 TEST_USERS = (
         User(uuid.UUID("ecb52977-3004-469e-9428-2a1856725c7f"), "group@lead.er",
-             "Group Leader"),
+             "Group Leader", created=_created_),
         User(uuid.UUID("21351b66-8aad-475b-84ac-53ce528451e3"),
-             "group@mem.ber01", "Group Member 01"),
+             "group@mem.ber01", "Group Member 01", created=_created_),
         User(uuid.UUID("ae9c6245-0966-41a5-9a5e-20885a96bea7"),
-             "group@mem.ber02", "Group Member 02"),
+             "group@mem.ber02", "Group Member 02", created=_created_),
         User(uuid.UUID("9a0c7ce5-2f40-4e78-979e-bf3527a59579"),
-             "unaff@iliated.user", "Unaffiliated User"))
+             "unaff@iliated.user", "Unaffiliated User", created=_created_),
+        User(uuid.UUID("60faf8a7-832b-471e-b6a0-bd4013f1fa0e"),
+             "sys@admin.user", "System Admin User", created=_created_))
 
 @pytest.fixture(scope="function")
-def fxtr_users(conn_after_auth_migrations, fxtr_group):# pylint: disable=[redefined-outer-name, unused-argument]
+def fxtr_users(conn_after_auth_migrations):# pylint: disable=[redefined-outer-name, unused-argument]
     """Fixture: setup test users."""
-    query = "INSERT INTO users(user_id, email, name) VALUES (?, ?, ?)"
+    query = (
+        "INSERT INTO users(user_id, email, name, created) VALUES (?, ?, ?, ?)")
     with db.cursor(conn_after_auth_migrations) as cursor:
         cursor.executemany(query, (
-            (str(user.user_id), user.email, user.name) for user in TEST_USERS))
+            (str(user.user_id), user.email, user.name, user.created.timestamp())
+            for user in TEST_USERS))
 
     yield (conn_after_auth_migrations, TEST_USERS)
 
diff --git a/tests/unit/auth/test_groups.py b/tests/unit/auth/test_groups.py
index 346beb9..6f1e8cd 100644
--- a/tests/unit/auth/test_groups.py
+++ b/tests/unit/auth/test_groups.py
@@ -61,6 +61,8 @@ def __cleanup_create_group__(conn, user, group):
             (str(user.user_id), str(grp_rsc["resource_id"])))
         cursor.execute("DELETE FROM group_resources WHERE group_id=?",
                        (str(group.group_id),))
+        cursor.execute("DELETE FROM resources WHERE resource_id=?",
+                       (grp_rsc["resource_id"],))
         cursor.execute("DELETE FROM groups WHERE group_id=?",
                        (str(group.group_id),))
 
diff --git a/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py b/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py
index c34a549..a32cacb 100644
--- a/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py
+++ b/tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py
@@ -8,7 +8,7 @@ from gn_auth.migrations import get_migration, apply_migrations, rollback_migrati
 from tests.unit.auth.conftest import (
     apply_single_migration, rollback_single_migration, migrations_up_to)
 
-MIGRATION_PATH = "migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py"
+MIGRATION_PATH = "gn_auth/migrations/auth/20221108_04_CKcSL-init-data-in-resource-categories-table.py"
 
 @pytest.mark.unit_test
 def test_apply_init_data(auth_testdb_path, auth_migrations_dir, backend):
diff --git a/tests/unit/auth/test_resources.py b/tests/unit/auth/test_resources.py
index 04da6df..81f967e 100644
--- a/tests/unit/auth/test_resources.py
+++ b/tests/unit/auth/test_resources.py
@@ -114,19 +114,19 @@ def test_public_resources(fxtr_resources):
     "user,expected",
     tuple(zip(
         conftest.TEST_USERS,
-        (sorted(
+        ((sorted(
             {res.resource_id: res for res in
              ((conftest.GROUP_RESOURCES[0],) +
               conftest.TEST_RESOURCES_GROUP_01 +
               conftest.TEST_RESOURCES_PUBLIC)}.values(),
-            key=sort_key_resources),
-         sorted(
+            key=sort_key_resources), 6),
+         (sorted(
              {res.resource_id: res for res in
               ((conftest.TEST_RESOURCES_GROUP_01[1],) +
                conftest.TEST_RESOURCES_PUBLIC)}.values()
              ,
-             key=sort_key_resources),
-         PUBLIC_RESOURCES, PUBLIC_RESOURCES))))
+             key=sort_key_resources), 4),
+         (PUBLIC_RESOURCES, 3), (PUBLIC_RESOURCES, 3)))))
 def test_user_resources(fxtr_resource_user_roles, user, expected):
     """
     GIVEN: some resources in the database
@@ -134,6 +134,10 @@ def test_user_resources(fxtr_resource_user_roles, user, expected):
     THEN: list only the resources for which the user can access
     """
     conn, *_others = fxtr_resource_user_roles
+    uresources, count = user_resources(conn, user)
+    eresources, ecount = expected
+    assert count == ecount
     assert sorted(
-        {res.resource_id: res for res in user_resources(conn, user)
-         }.values(), key=sort_key_resources) == expected
+        {res.resource_id: res for res in  uresources}.values(),
+        key=sort_key_resources
+    ) == eresources