mirror of
https://github.com/ansible/awx.git
synced 2026-03-04 18:21:03 -03:30
AAP-48070 Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management (#16033)
Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and enable local resource management This commit removes the ALLOW_LOCAL_RESOURCE_MANAGEMENT setting and all associated functionality, making the behavior as if the setting is always enabled. Changes: - Remove ALLOW_LOCAL_RESOURCE_MANAGEMENT setting from defaults.py - Remove @immutablesharedfields decorator and all related logic - Remove decorator applications from Organization, Team, and User API views - Remove role assignment restrictions in UserRolesList and RoleUsersList - Remove test file for immutablesharedfields functionality - Clean up unused imports Result: Organizations, Teams, and Users can now always be created, modified, and deleted via the API without platform ingress restrictions.
This commit is contained in:
@@ -56,7 +56,6 @@ from wsgiref.util import FileWrapper
|
|||||||
# django-ansible-base
|
# django-ansible-base
|
||||||
from ansible_base.lib.utils.requests import get_remote_hosts
|
from ansible_base.lib.utils.requests import get_remote_hosts
|
||||||
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
||||||
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
|
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
||||||
@@ -671,81 +670,16 @@ class ScheduleUnifiedJobsList(SubListAPIView):
|
|||||||
name = _('Schedule Jobs List')
|
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):
|
class TeamList(ListCreateAPIView):
|
||||||
model = models.Team
|
model = models.Team
|
||||||
serializer_class = serializers.TeamSerializer
|
serializer_class = serializers.TeamSerializer
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class TeamDetail(RetrieveUpdateDestroyAPIView):
|
class TeamDetail(RetrieveUpdateDestroyAPIView):
|
||||||
model = models.Team
|
model = models.Team
|
||||||
serializer_class = serializers.TeamSerializer
|
serializer_class = serializers.TeamSerializer
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class TeamUsersList(BaseUsersList):
|
class TeamUsersList(BaseUsersList):
|
||||||
model = models.User
|
model = models.User
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
@@ -1127,7 +1061,6 @@ class ProjectCopy(CopyAPIView):
|
|||||||
copy_return_serializer_class = serializers.ProjectSerializer
|
copy_return_serializer_class = serializers.ProjectSerializer
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class UserList(ListCreateAPIView):
|
class UserList(ListCreateAPIView):
|
||||||
model = models.User
|
model = models.User
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
@@ -1184,14 +1117,6 @@ class UserRolesList(SubListAttachDetachAPIView):
|
|||||||
role = get_object_or_400(models.Role, pk=sub_id)
|
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}
|
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]
|
credential_content_type = content_types[models.Credential]
|
||||||
if role.content_type == credential_content_type:
|
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:
|
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]))
|
return qs.filter(Q(actor=parent) | Q(user__in=[parent]))
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class UserDetail(RetrieveUpdateDestroyAPIView):
|
class UserDetail(RetrieveUpdateDestroyAPIView):
|
||||||
model = models.User
|
model = models.User
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
@@ -4239,13 +4163,6 @@ class RoleUsersList(SubListAttachDetachAPIView):
|
|||||||
role = self.get_parent_object()
|
role = self.get_parent_object()
|
||||||
|
|
||||||
content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type}
|
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]
|
credential_content_type = content_types[models.Credential]
|
||||||
if role.content_type == credential_content_type:
|
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:
|
if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
|
||||||
|
|||||||
@@ -53,18 +53,15 @@ from awx.api.serializers import (
|
|||||||
CredentialSerializer,
|
CredentialSerializer,
|
||||||
)
|
)
|
||||||
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin, OrganizationInstanceGroupMembershipMixin
|
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, OrganizationCountsMixin, OrganizationInstanceGroupMembershipMixin
|
||||||
from awx.api.views import immutablesharedfields
|
|
||||||
|
|
||||||
logger = logging.getLogger('awx.api.views.organization')
|
logger = logging.getLogger('awx.api.views.organization')
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
|
class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
|
||||||
model = Organization
|
model = Organization
|
||||||
serializer_class = OrganizationSerializer
|
serializer_class = OrganizationSerializer
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
||||||
model = Organization
|
model = Organization
|
||||||
serializer_class = OrganizationSerializer
|
serializer_class = OrganizationSerializer
|
||||||
@@ -107,7 +104,6 @@ class OrganizationInventoriesList(SubListAPIView):
|
|||||||
relationship = 'inventories'
|
relationship = 'inventories'
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class OrganizationUsersList(BaseUsersList):
|
class OrganizationUsersList(BaseUsersList):
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
@@ -116,7 +112,6 @@ class OrganizationUsersList(BaseUsersList):
|
|||||||
ordering = ('username',)
|
ordering = ('username',)
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class OrganizationAdminsList(BaseUsersList):
|
class OrganizationAdminsList(BaseUsersList):
|
||||||
model = User
|
model = User
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
@@ -155,7 +150,6 @@ class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView):
|
|||||||
parent_key = 'organization'
|
parent_key = 'organization'
|
||||||
|
|
||||||
|
|
||||||
@immutablesharedfields
|
|
||||||
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
|
||||||
model = Team
|
model = Team
|
||||||
serializer_class = TeamSerializer
|
serializer_class = TeamSerializer
|
||||||
|
|||||||
@@ -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']
|
|
||||||
@@ -538,9 +538,6 @@ AWX_ANSIBLE_CALLBACK_PLUGINS = ""
|
|||||||
# Automatically remove nodes that have missed their heartbeats after some time
|
# Automatically remove nodes that have missed their heartbeats after some time
|
||||||
AWX_AUTO_DEPROVISION_INSTANCES = False
|
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
|
# If True, allow users to be assigned to roles that were created via JWT
|
||||||
ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False
|
ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False
|
||||||
|
|||||||
Reference in New Issue
Block a user