From 8fd6f2590ca3bb869508fc5c92bd7ce63a567987 Mon Sep 17 00:00:00 2001 From: David Newswanger Date: Tue, 4 Jun 2024 10:01:06 -0600 Subject: [PATCH] Add option to disable creating, update and deleting users and groups. Issue: AAP-24300 --- CHANGES/24300.misc | 1 + galaxy_ng/app/access_control/access_policy.py | 29 ++-- .../app/access_control/statements/pulp.py | 28 +++- .../access_control/statements/standalone.py | 124 +++++++++------ galaxy_ng/app/settings.py | 5 +- .../api/test_disable_shared_resources.py | 148 ++++++++++++++++++ profiles/dab/pulp_config.env | 4 + profiles/dab_jwt/pulp_config.env | 3 + 8 files changed, 279 insertions(+), 63 deletions(-) create mode 100644 CHANGES/24300.misc create mode 100644 galaxy_ng/tests/integration/api/test_disable_shared_resources.py diff --git a/CHANGES/24300.misc b/CHANGES/24300.misc new file mode 100644 index 0000000000..5000aa1850 --- /dev/null +++ b/CHANGES/24300.misc @@ -0,0 +1 @@ +Add option to disable creating, update and deleting users and groups. \ No newline at end of file diff --git a/galaxy_ng/app/access_control/access_policy.py b/galaxy_ng/app/access_control/access_policy.py index b2d91c4443..448e804df6 100644 --- a/galaxy_ng/app/access_control/access_policy.py +++ b/galaxy_ng/app/access_control/access_policy.py @@ -573,6 +573,22 @@ def require_requirements_yaml(self, request, view, action): }) return True + def is_direct_shared_resource_management_disabled(self, request, view, action): + return not settings.DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED + + def user_is_superuser(self, request, view, action): + if getattr(self, "swagger_fake_view", False): + # If OpenAPI schema is requested, don't check for superuser + return False + user = view.get_object() + return user.is_superuser + + def is_current_user(self, request, view, action): + if getattr(self, "swagger_fake_view", False): + # If OpenAPI schema is requested, don't check for current user + return False + return request.user == view.get_object() + class AIDenyIndexAccessPolicy(AccessPolicyBase): NAME = "AIDenyIndexView" @@ -613,19 +629,6 @@ class CollectionRemoteAccessPolicy(AccessPolicyBase): class UserAccessPolicy(AccessPolicyBase): NAME = "UserViewSet" - def user_is_superuser(self, request, view, action): - if getattr(self, "swagger_fake_view", False): - # If OpenAPI schema is requested, don't check for superuser - return False - user = view.get_object() - return user.is_superuser - - def is_current_user(self, request, view, action): - if getattr(self, "swagger_fake_view", False): - # If OpenAPI schema is requested, don't check for current user - return False - return request.user == view.get_object() - class MyUserAccessPolicy(AccessPolicyBase): NAME = "MyUserViewSet" diff --git a/galaxy_ng/app/access_control/statements/pulp.py b/galaxy_ng/app/access_control/statements/pulp.py index 42b1878999..cb18b3629a 100644 --- a/galaxy_ng/app/access_control/statements/pulp.py +++ b/galaxy_ng/app/access_control/statements/pulp.py @@ -1,8 +1,10 @@ from galaxy_ng.app.access_control.statements.standalone import ( _collection_statements as _galaxy_collection_statements, _group_statements as _galaxy_group_statements, + _group_role_statements as _galaxy_group_role_statements ) +from galaxy_ng.app.access_control.statements.standalone import _user_statements _collection_statements = {"statements": _galaxy_collection_statements} @@ -10,6 +12,9 @@ _group_statements = {"statements": _galaxy_group_statements} +_group_role_statements = {"statements": _galaxy_group_role_statements} + + _deny_all = { "statements": [ {"principal": "*", "action": "*", "effect": "deny"}, @@ -565,8 +570,29 @@ PULP_CORE_VIEWSETS = { - "groups/roles": _group_statements, + "groups/roles": _group_role_statements, "groups": _group_statements, + "groups/users": {"statements": [ + # We didn't have an access policy here before 4.10. The default pulp access policy + # checks core.group permissions, rather than galaxy.group permissions, which isn't + # used in our system. The end result should be that only admins can make modifications + # on this endpoint. This should be changed to match the validation we use for the + # ui apis (https://github.com/ansible/galaxy_ng/blob/7e6b335326fd1d1f366e3c5dd81b3f6e + # 75da9e1e/galaxy_ng/app/api/ui/serializers/user.py#L62), but given that we're looking + # at adopting DAB RBAC, I'm going to leave this as is for now. + { + "action": "*", + "principal": "admin", + "effect": "allow" + }, + { + "action": ["create", "destroy"], + "principal": "*", + "effect": "deny", + "condition": "is_direct_shared_resource_management_disabled" + }, + ]}, + "users": {"statements": _user_statements}, "roles": { "statements": [ { diff --git a/galaxy_ng/app/access_control/statements/standalone.py b/galaxy_ng/app/access_control/statements/standalone.py index c77a7ada3a..8e4cb1ea11 100644 --- a/galaxy_ng/app/access_control/statements/standalone.py +++ b/galaxy_ng/app/access_control/statements/standalone.py @@ -76,7 +76,7 @@ } ] -_group_statements = [ +_group_role_statements = [ { "action": ["list", "retrieve"], "principal": "authenticated", @@ -86,22 +86,87 @@ "action": "destroy", "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:galaxy.delete_group" + "condition": [ + "has_model_perms:galaxy.delete_group", + ] }, { "action": "create", "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:galaxy.add_group" + "condition": [ + "has_model_perms:galaxy.add_group", + ] }, { "action": ["update", "partial_update"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:galaxy.update_group" + "condition": [ + "has_model_perms:galaxy.update_group", + ] }, ] +_group_statements = _group_role_statements + [ + { + "action": ["create", "destroy", "update", "partial_update"], + "principal": "*", + "effect": "deny", + "condition": "is_direct_shared_resource_management_disabled" + }, +] + +_user_statements = [ + { + "action": ["list"], + "principal": "authenticated", + "effect": "allow", + "condition": ["v3_can_view_users"], + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": ["v3_can_view_users"], + }, + { + "action": "destroy", + "principal": "*", + "effect": "deny", + "condition": ["user_is_superuser"] + }, + { + "action": "destroy", + "principal": "*", + "effect": "deny", + "condition": ["is_current_user"] + }, + { + "action": "destroy", + "principal": "*", + "effect": "allow", + "condition": "has_model_perms:galaxy.delete_user" + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.add_user" + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:galaxy.change_user" + }, + { + "action": ["create", "destroy", "update", "partial_update"], + "principal": "*", + "effect": "deny", + "condition": "is_direct_shared_resource_management_disabled" + }, +] _deny_all = [ { "principal": "*", @@ -187,50 +252,7 @@ "condition": "has_model_perms:ansible.change_collectionremote" } ], - 'UserViewSet': [ - { - "action": ["list"], - "principal": "authenticated", - "effect": "allow", - "condition": ["v3_can_view_users"], - }, - { - "action": ["retrieve"], - "principal": "authenticated", - "effect": "allow", - "condition": ["v3_can_view_users"], - }, - { - "action": "destroy", - "principal": "*", - "effect": "deny", - "condition": ["user_is_superuser"] - }, - { - "action": "destroy", - "principal": "*", - "effect": "deny", - "condition": ["is_current_user"] - }, - { - "action": "destroy", - "principal": "*", - "effect": "allow", - "condition": "has_model_perms:galaxy.delete_user" - }, - { - "action": "create", - "principal": "authenticated", - "effect": "allow", - "condition": "has_model_perms:galaxy.add_user" - }, - { - "action": ["update", "partial_update"], - "principal": "authenticated", - "effect": "allow", - "condition": "has_model_perms:galaxy.change_user" - }, - ], + 'UserViewSet': _user_statements, 'MyUserViewSet': [ { "action": ["retrieve"], @@ -244,6 +266,12 @@ "effect": "allow", "condition": "is_current_user" }, + { + "action": ["create", "destroy", "update", "partial_update"], + "principal": "*", + "effect": "deny", + "condition": "is_direct_shared_resource_management_disabled" + }, ], # disable synclists for on prem installations 'SyncListViewSet': _deny_all, diff --git a/galaxy_ng/app/settings.py b/galaxy_ng/app/settings.py index b700c41c3c..e63566cca9 100644 --- a/galaxy_ng/app/settings.py +++ b/galaxy_ng/app/settings.py @@ -77,7 +77,7 @@ # Galaxy authentication classes are used to set REST_FRAMEWORK__DEFAULT_AUTHENTICATION_CLASSES GALAXY_AUTHENTICATION_CLASSES = [ - "galaxy_ng.app.auth.session.SessionAuthentication", + "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.BasicAuthentication", "ansible_base.jwt_consumer.hub.auth.HubJWTAuth", @@ -308,3 +308,6 @@ # WARNING: This setting is used in database migrations to create a default organization. DEFAULT_ORGANIZATION_NAME = "Default" + +# Disables editing and managing users and groups. +DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED = True diff --git a/galaxy_ng/tests/integration/api/test_disable_shared_resources.py b/galaxy_ng/tests/integration/api/test_disable_shared_resources.py new file mode 100644 index 0000000000..021e44e057 --- /dev/null +++ b/galaxy_ng/tests/integration/api/test_disable_shared_resources.py @@ -0,0 +1,148 @@ +import os +import pytest +from galaxykit.utils import GalaxyClientError +import uuid + + +@pytest.fixture +def test_group(galaxy_client): + gc = galaxy_client("admin") + + return gc.get("_ui/v1/groups/?name=ns_group_for_tests")["data"][0] + + +@pytest.fixture +def test_user(galaxy_client): + gc = galaxy_client("admin") + + return gc.get("_ui/v1/users/?username=admin")["data"][0] + + +@pytest.mark.parametrize( + 'url', + [ + "_ui/v1/groups/", + "pulp/api/v3/groups/", + ] +) +@pytest.mark.deployment_standalone +@pytest.mark.skipif( + not os.getenv("ENABLE_DAB_TESTS"), + reason="Skipping test because ENABLE_DAB_TESTS is not set" +) +def test_dab_groups_are_read_only(galaxy_client, url, test_group): + gc = galaxy_client("admin") + + group_pk = test_group["id"] + + with pytest.raises(GalaxyClientError) as ctx: + gc.post(url, body={"name": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + # Apparently we don't support updating on the ui api? + if "pulp/api" in url: + detail = url + f"{group_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.patch(detail, body={"name": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + detail = url + f"{group_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.put(detail, body={"name": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + detail = url + f"{group_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.delete(detail) + + assert ctx.value.args[0] == 403 + + +@pytest.mark.parametrize( + 'url', + [ + "_ui/v1/users/", + "pulp/api/v3/users/", + ] +) +@pytest.mark.deployment_standalone +@pytest.mark.skipif( + not os.getenv("ENABLE_DAB_TESTS"), + reason="Skipping test because ENABLE_DAB_TESTS is not set" +) +def test_dab_users_are_read_only(galaxy_client, url, test_user): + gc = galaxy_client("admin") + + user_pk = test_user["id"] + + with pytest.raises(GalaxyClientError) as ctx: + gc.post(url, body={"username": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + detail = url + f"{user_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.patch(detail, body={"username": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + detail = url + f"{user_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.put(detail, body={"username": str(uuid.uuid4())}) + + assert ctx.value.args[0] == 403 + + detail = url + f"{user_pk}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.delete(detail) + + assert ctx.value.args[0] == 403 + + +@pytest.mark.deployment_standalone +@pytest.mark.skipif( + not os.getenv("ENABLE_DAB_TESTS"), + reason="Skipping test because ENABLE_DAB_TESTS is not set" +) +def test_dab_cant_modify_group_memberships(galaxy_client, test_user, test_group): + gc = galaxy_client("admin") + + hub_user_detail = f"_ui/v1/users/{test_user['id']}/" + with pytest.raises(GalaxyClientError) as ctx: + gc.patch(hub_user_detail, body={ + "groups": [{ + "id": test_group["id"], + "name": test_group["name"], + }] + }) + + assert ctx.value.args[0] == 403 + + pulp_group_users = f"pulp/api/v3/groups/{test_group['id']}/users/" + + with pytest.raises(GalaxyClientError) as ctx: + gc.post(pulp_group_users, body={"username": test_user["username"]}) + + assert ctx.value.args[0] == 403 + + +@pytest.mark.deployment_standalone +@pytest.mark.skipif( + not os.getenv("ENABLE_DAB_TESTS"), + reason="Skipping test because ENABLE_DAB_TESTS is not set" +) +def test_dab_can_modify_roles(galaxy_client, test_user, test_group): + gc = galaxy_client("admin") + + gc.post(f"pulp/api/v3/groups/{test_group['id']}/roles/", body={ + "content_object": None, + "role": "galaxy.content_admin", + }) + + gc.post(f"pulp/api/v3/users/{test_user['id']}/roles/", body={ + "content_object": None, + "role": "galaxy.content_admin", + }) diff --git a/profiles/dab/pulp_config.env b/profiles/dab/pulp_config.env index 99482d7ae0..334e029644 100644 --- a/profiles/dab/pulp_config.env +++ b/profiles/dab/pulp_config.env @@ -19,3 +19,7 @@ PULP_GALAXY_CONTAINER_SIGNING_SERVICE=container-default # dynamic download urls PULP_DYNACONF_AFTER_GET_HOOKS=["read_settings_from_cache_or_db", "alter_hostname_settings"] +PULP_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED=false + +# disable user/group modifications +PULP_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED=false diff --git a/profiles/dab_jwt/pulp_config.env b/profiles/dab_jwt/pulp_config.env index ea987b2885..3f663864f8 100644 --- a/profiles/dab_jwt/pulp_config.env +++ b/profiles/dab_jwt/pulp_config.env @@ -19,3 +19,6 @@ PULP_GALAXY_CONTAINER_SIGNING_SERVICE=container-default # dynamic download urls PULP_DYNACONF_AFTER_GET_HOOKS=["read_settings_from_cache_or_db", "alter_hostname_settings"] + +# disable user/group modifications +PULP_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED=false