diff options
| -rw-r--r-- | .guix/modules/gn-auth.scm | 2 | ||||
| -rw-r--r-- | .pylintrc | 13 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | gn_auth/__init__.py | 40 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/endpoints/introspection.py | 2 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/endpoints/revocation.py | 2 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/endpoints/utilities.py | 6 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/grants/jwt_bearer_grant.py | 12 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/models/oauth2client.py | 17 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/resource_server.py | 6 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/oauth2/views.py | 7 | ||||
| -rw-r--r-- | gn_auth/auth/authentication/users.py | 4 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/genotypes.py | 41 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/mrna.py | 40 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/phenotypes.py | 81 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/data/views.py | 276 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/base.py | 52 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/checks.py | 41 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/groups/models.py | 17 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/models.py | 130 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/system/views.py | 27 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/resources/views.py | 80 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/admin/models.py | 11 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/admin/views.py | 8 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/collections/views.py | 5 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/models.py | 38 | ||||
| -rw-r--r-- | gn_auth/auth/authorisation/users/views.py | 22 | ||||
| -rw-r--r-- | gn_auth/auth/db/sqlite3.py | 61 | ||||
| -rw-r--r-- | gn_auth/auth/errors.py | 2 | ||||
| -rw-r--r-- | gn_auth/debug.py | 22 | ||||
| -rw-r--r-- | gn_auth/errors/authlib.py | 2 | ||||
| -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.py | 69 | ||||
| -rw-r--r-- | gn_auth/migrations/auth/20260402_01_Bf8nm-add-user-and-time-tracking-to-resources-table.py | 185 | ||||
| -rw-r--r-- | gn_auth/migrations/auth/20260428_01_Tak6O-new-privilege-system-system-wide-data-view.py | 19 | ||||
| -rw-r--r-- | gn_auth/migrations/auth/20260428_02_L6zIV-add-privileges-to-batch-editors-role.py | 62 | ||||
| -rw-r--r-- | gn_auth/migrations/auth/__init__.py (renamed from migrations/auth/__init__.py) | 0 | ||||
| -rw-r--r-- | gn_auth/scripts/__init__.py | 1 | ||||
| -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.py | 4 | ||||
| -rw-r--r-- | gn_auth/wsgi.py | 377 | ||||
| -rw-r--r-- | migrations/__init__.py | 1 | ||||
| -rw-r--r-- | pyproject.toml | 71 | ||||
| -rw-r--r-- | scripts/__init__.py | 0 | ||||
| -rw-r--r-- | setup.cfg | 4 | ||||
| -rwxr-xr-x | setup.py | 48 | ||||
| -rw-r--r-- | setup_commands/__init__.py | 3 | ||||
| -rw-r--r-- | setup_commands/run_tests.py | 40 | ||||
| -rw-r--r-- | tests/unit/auth/fixtures/group_fixtures.py | 21 | ||||
| -rw-r--r-- | tests/unit/auth/fixtures/resource_fixtures.py | 48 | ||||
| -rw-r--r-- | tests/unit/auth/fixtures/role_fixtures.py | 2 | ||||
| -rw-r--r-- | tests/unit/auth/fixtures/user_fixtures.py | 21 | ||||
| -rw-r--r-- | tests/unit/auth/test_groups.py | 2 | ||||
| -rw-r--r-- | tests/unit/auth/test_migrations_init_data_in_resource_categories_table.py | 2 | ||||
| -rw-r--r-- | tests/unit/auth/test_resources.py | 18 |
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 |
