diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index ec8baa82e4..ea03d25c93 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -56,7 +56,6 @@ from wsgiref.util import FileWrapper # django-ansible-base from ansible_base.lib.utils.requests import get_remote_hosts from ansible_base.rbac.models import RoleEvaluation, ObjectRole -from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType # AWX from awx.main.tasks.system import send_notifications, update_inventory_computed_fields @@ -671,81 +670,16 @@ class ScheduleUnifiedJobsList(SubListAPIView): name = _('Schedule Jobs List') -def immutablesharedfields(cls): - ''' - Class decorator to prevent modifying shared resources when ALLOW_LOCAL_RESOURCE_MANAGEMENT setting is set to False. - - Works by overriding these view methods: - - create - - delete - - perform_update - create and delete are overridden to raise a PermissionDenied exception. - perform_update is overridden to check if any shared fields are being modified, - and raise a PermissionDenied exception if so. - ''' - # create instead of perform_create because some of our views - # override create instead of perform_create - if hasattr(cls, 'create'): - cls.original_create = cls.create - - @functools.wraps(cls.create) - def create_wrapper(*args, **kwargs): - if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - return cls.original_create(*args, **kwargs) - raise PermissionDenied({'detail': _('Creation of this resource is not allowed. Create this resource via the platform ingress.')}) - - cls.create = create_wrapper - - if hasattr(cls, 'delete'): - cls.original_delete = cls.delete - - @functools.wraps(cls.delete) - def delete_wrapper(*args, **kwargs): - if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - return cls.original_delete(*args, **kwargs) - raise PermissionDenied({'detail': _('Deletion of this resource is not allowed. Delete this resource via the platform ingress.')}) - - cls.delete = delete_wrapper - - if hasattr(cls, 'perform_update'): - cls.original_perform_update = cls.perform_update - - @functools.wraps(cls.perform_update) - def update_wrapper(*args, **kwargs): - if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - view, serializer = args - instance = view.get_object() - if instance: - if isinstance(instance, models.Organization): - shared_fields = OrganizationType._declared_fields.keys() - elif isinstance(instance, models.User): - shared_fields = UserType._declared_fields.keys() - elif isinstance(instance, models.Team): - shared_fields = TeamType._declared_fields.keys() - attrs = serializer.validated_data - for field in shared_fields: - if field in attrs and getattr(instance, field) != attrs[field]: - raise PermissionDenied({field: _(f"Cannot change shared field '{field}'. Alter this field via the platform ingress.")}) - return cls.original_perform_update(*args, **kwargs) - - cls.perform_update = update_wrapper - - return cls - - -@immutablesharedfields class TeamList(ListCreateAPIView): model = models.Team serializer_class = serializers.TeamSerializer -@immutablesharedfields class TeamDetail(RetrieveUpdateDestroyAPIView): model = models.Team serializer_class = serializers.TeamSerializer -@immutablesharedfields class TeamUsersList(BaseUsersList): model = models.User serializer_class = serializers.UserSerializer @@ -1127,7 +1061,6 @@ class ProjectCopy(CopyAPIView): copy_return_serializer_class = serializers.ProjectSerializer -@immutablesharedfields class UserList(ListCreateAPIView): model = models.User serializer_class = serializers.UserSerializer @@ -1184,14 +1117,6 @@ class UserRolesList(SubListAttachDetachAPIView): role = get_object_or_400(models.Role, pk=sub_id) content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type} - # Prevent user to be associated with team/org when ALLOW_LOCAL_RESOURCE_MANAGEMENT is False - if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - for model in [models.Organization, models.Team]: - ct = content_types[model] - if role.content_type == ct and role.role_field in ['member_role', 'admin_role']: - data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled")) - return Response(data, status=status.HTTP_403_FORBIDDEN) - credential_content_type = content_types[models.Credential] if role.content_type == credential_content_type: if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role: @@ -1264,7 +1189,6 @@ class UserActivityStreamList(SubListAPIView): return qs.filter(Q(actor=parent) | Q(user__in=[parent])) -@immutablesharedfields class UserDetail(RetrieveUpdateDestroyAPIView): model = models.User serializer_class = serializers.UserSerializer @@ -4239,13 +4163,6 @@ class RoleUsersList(SubListAttachDetachAPIView): role = self.get_parent_object() content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type} - if not settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - for model in [models.Organization, models.Team]: - ct = content_types[model] - if role.content_type == ct and role.role_field in ['member_role', 'admin_role']: - data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled")) - return Response(data, status=status.HTTP_403_FORBIDDEN) - credential_content_type = content_types[models.Credential] if role.content_type == credential_content_type: if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role: diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 338a35678b..021f90a738 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -53,18 +53,15 @@ from awx.api.serializers import ( CredentialSerializer, ) from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin, OrganizationInstanceGroupMembershipMixin -from awx.api.views import immutablesharedfields logger = logging.getLogger('awx.api.views.organization') -@immutablesharedfields class OrganizationList(OrganizationCountsMixin, ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer -@immutablesharedfields class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = Organization serializer_class = OrganizationSerializer @@ -107,7 +104,6 @@ class OrganizationInventoriesList(SubListAPIView): relationship = 'inventories' -@immutablesharedfields class OrganizationUsersList(BaseUsersList): model = User serializer_class = UserSerializer @@ -116,7 +112,6 @@ class OrganizationUsersList(BaseUsersList): ordering = ('username',) -@immutablesharedfields class OrganizationAdminsList(BaseUsersList): model = User serializer_class = UserSerializer @@ -155,7 +150,6 @@ class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView): parent_key = 'organization' -@immutablesharedfields class OrganizationTeamsList(SubListCreateAttachDetachAPIView): model = Team serializer_class = TeamSerializer diff --git a/awx/main/tests/functional/api/test_immutablesharedfields.py b/awx/main/tests/functional/api/test_immutablesharedfields.py deleted file mode 100644 index e9c1f1b06f..0000000000 --- a/awx/main/tests/functional/api/test_immutablesharedfields.py +++ /dev/null @@ -1,64 +0,0 @@ -import pytest - -from awx.api.versioning import reverse -from awx.main.models import Organization - - -@pytest.mark.django_db -class TestImmutableSharedFields: - @pytest.fixture(autouse=True) - def configure_settings(self, settings): - settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT = False - - def test_create_raises_permission_denied(self, admin_user, post): - orgA = Organization.objects.create(name='orgA') - resp = post( - url=reverse('api:team_list'), - data={'name': 'teamA', 'organization': orgA.id}, - user=admin_user, - expect=403, - ) - assert "Creation of this resource is not allowed" in resp.data['detail'] - - def test_perform_delete_raises_permission_denied(self, admin_user, delete): - orgA = Organization.objects.create(name='orgA') - team = orgA.teams.create(name='teamA') - resp = delete( - url=reverse('api:team_detail', kwargs={'pk': team.id}), - user=admin_user, - expect=403, - ) - assert "Deletion of this resource is not allowed" in resp.data['detail'] - - def test_perform_update(self, admin_user, patch): - orgA = Organization.objects.create(name='orgA') - # allow patching non-shared fields - patch( - url=reverse('api:organization_detail', kwargs={'pk': orgA.id}), - data={"max_hosts": 76}, - user=admin_user, - expect=200, - ) - # prevent patching shared fields - resp = patch(url=reverse('api:organization_detail', kwargs={'pk': orgA.id}), data={"name": "orgB"}, user=admin_user, expect=403) - assert "Cannot change shared field" in resp.data['name'] - - @pytest.mark.parametrize( - 'role', - ['admin_role', 'member_role'], - ) - @pytest.mark.parametrize('resource', ['organization', 'team']) - def test_prevent_assigning_member_to_organization_or_team(self, admin_user, post, resource, role): - orgA = Organization.objects.create(name='orgA') - if resource == 'organization': - role = getattr(orgA, role) - elif resource == 'team': - teamA = orgA.teams.create(name='teamA') - role = getattr(teamA, role) - resp = post( - url=reverse('api:user_roles_list', kwargs={'pk': admin_user.id}), - data={'id': role.id}, - user=admin_user, - expect=403, - ) - assert f"Cannot directly modify user membership to {resource}." in resp.data['msg'] diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 44ea309d10..740758bee1 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -538,9 +538,6 @@ AWX_ANSIBLE_CALLBACK_PLUGINS = "" # Automatically remove nodes that have missed their heartbeats after some time AWX_AUTO_DEPROVISION_INSTANCES = False -# If False, do not allow creation of resources that are shared with the platform ingress -# e.g. organizations, teams, and users -ALLOW_LOCAL_RESOURCE_MANAGEMENT = True # If True, allow users to be assigned to roles that were created via JWT ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False