|
62 | 62 |
|
63 | 63 | # django-ansible-base
|
64 | 64 | from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
| 65 | +from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType |
65 | 66 |
|
66 | 67 | # AWX
|
67 | 68 | from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
|
128 | 129 | from awx.api.pagination import UnifiedJobEventPagination
|
129 | 130 | from awx.main.utils import set_environ
|
130 | 131 |
|
| 132 | + |
131 | 133 | logger = logging.getLogger('awx.api.views')
|
132 | 134 |
|
133 | 135 |
|
@@ -710,16 +712,81 @@ def get(self, request):
|
710 | 712 | return Response(data)
|
711 | 713 |
|
712 | 714 |
|
| 715 | +def immutablesharedfields(cls): |
| 716 | + ''' |
| 717 | + Class decorator to prevent modifying shared resources when AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED setting is set to False. |
| 718 | +
|
| 719 | + Works by overriding these view methods: |
| 720 | + - create |
| 721 | + - delete |
| 722 | + - perform_update |
| 723 | + create and delete are overridden to raise a PermissionDenied exception. |
| 724 | + perform_update is overridden to check if any shared fields are being modified, |
| 725 | + and raise a PermissionDenied exception if so. |
| 726 | + ''' |
| 727 | + # create instead of perform_create because some of our views |
| 728 | + # override create instead of perform_create |
| 729 | + if hasattr(cls, 'create'): |
| 730 | + cls.original_create = cls.create |
| 731 | + |
| 732 | + @functools.wraps(cls.create) |
| 733 | + def create_wrapper(*args, **kwargs): |
| 734 | + if settings.AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED: |
| 735 | + return cls.original_create(*args, **kwargs) |
| 736 | + raise PermissionDenied({'detail': _('Creation of this resource is not allowed. Create this resource via the platform ingress.')}) |
| 737 | + |
| 738 | + cls.create = create_wrapper |
| 739 | + |
| 740 | + if hasattr(cls, 'delete'): |
| 741 | + cls.original_delete = cls.delete |
| 742 | + |
| 743 | + @functools.wraps(cls.delete) |
| 744 | + def delete_wrapper(*args, **kwargs): |
| 745 | + if settings.AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED: |
| 746 | + return cls.original_delete(*args, **kwargs) |
| 747 | + raise PermissionDenied({'detail': _('Deletion of this resource is not allowed. Delete this resource via the platform ingress.')}) |
| 748 | + |
| 749 | + cls.delete = delete_wrapper |
| 750 | + |
| 751 | + if hasattr(cls, 'perform_update'): |
| 752 | + cls.original_perform_update = cls.perform_update |
| 753 | + |
| 754 | + @functools.wraps(cls.perform_update) |
| 755 | + def update_wrapper(*args, **kwargs): |
| 756 | + if not settings.AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED: |
| 757 | + view, serializer = args |
| 758 | + instance = view.get_object() |
| 759 | + if instance: |
| 760 | + if isinstance(instance, models.Organization): |
| 761 | + shared_fields = OrganizationType._declared_fields.keys() |
| 762 | + elif isinstance(instance, models.User): |
| 763 | + shared_fields = UserType._declared_fields.keys() |
| 764 | + elif isinstance(instance, models.Team): |
| 765 | + shared_fields = TeamType._declared_fields.keys() |
| 766 | + attrs = serializer.validated_data |
| 767 | + for field in shared_fields: |
| 768 | + if field in attrs and getattr(instance, field) != attrs[field]: |
| 769 | + raise PermissionDenied({field: _(f"Cannot change shared field '{field}'. Alter this field via the platform ingress.")}) |
| 770 | + return cls.original_perform_update(*args, **kwargs) |
| 771 | + |
| 772 | + cls.perform_update = update_wrapper |
| 773 | + |
| 774 | + return cls |
| 775 | + |
| 776 | + |
| 777 | +@immutablesharedfields |
713 | 778 | class TeamList(ListCreateAPIView):
|
714 | 779 | model = models.Team
|
715 | 780 | serializer_class = serializers.TeamSerializer
|
716 | 781 |
|
717 | 782 |
|
| 783 | +@immutablesharedfields |
718 | 784 | class TeamDetail(RetrieveUpdateDestroyAPIView):
|
719 | 785 | model = models.Team
|
720 | 786 | serializer_class = serializers.TeamSerializer
|
721 | 787 |
|
722 | 788 |
|
| 789 | +@immutablesharedfields |
723 | 790 | class TeamUsersList(BaseUsersList):
|
724 | 791 | model = models.User
|
725 | 792 | serializer_class = serializers.UserSerializer
|
@@ -1101,6 +1168,7 @@ class ProjectCopy(CopyAPIView):
|
1101 | 1168 | copy_return_serializer_class = serializers.ProjectSerializer
|
1102 | 1169 |
|
1103 | 1170 |
|
| 1171 | +@immutablesharedfields |
1104 | 1172 | class UserList(ListCreateAPIView):
|
1105 | 1173 | model = models.User
|
1106 | 1174 | serializer_class = serializers.UserSerializer
|
@@ -1271,7 +1339,16 @@ def post(self, request, *args, **kwargs):
|
1271 | 1339 | user = get_object_or_400(models.User, pk=self.kwargs['pk'])
|
1272 | 1340 | role = get_object_or_400(models.Role, pk=sub_id)
|
1273 | 1341 |
|
1274 |
| - credential_content_type = ContentType.objects.get_for_model(models.Credential) |
| 1342 | + content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type} |
| 1343 | + # Prevent user to be associated with team/org when AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED is False |
| 1344 | + if not settings.AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED: |
| 1345 | + for model in [models.Organization, models.Team]: |
| 1346 | + ct = content_types[model] |
| 1347 | + if role.content_type == ct and role.role_field in ['member_role', 'admin_role']: |
| 1348 | + data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled")) |
| 1349 | + return Response(data, status=status.HTTP_403_FORBIDDEN) |
| 1350 | + |
| 1351 | + credential_content_type = content_types[models.Credential] |
1275 | 1352 | if role.content_type == credential_content_type:
|
1276 | 1353 | if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
|
1277 | 1354 | data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
|
@@ -1343,6 +1420,7 @@ def get_queryset(self):
|
1343 | 1420 | return qs.filter(Q(actor=parent) | Q(user__in=[parent]))
|
1344 | 1421 |
|
1345 | 1422 |
|
| 1423 | +@immutablesharedfields |
1346 | 1424 | class UserDetail(RetrieveUpdateDestroyAPIView):
|
1347 | 1425 | model = models.User
|
1348 | 1426 | serializer_class = serializers.UserSerializer
|
@@ -4295,7 +4373,15 @@ def post(self, request, *args, **kwargs):
|
4295 | 4373 | user = get_object_or_400(models.User, pk=sub_id)
|
4296 | 4374 | role = self.get_parent_object()
|
4297 | 4375 |
|
4298 |
| - credential_content_type = ContentType.objects.get_for_model(models.Credential) |
| 4376 | + content_types = ContentType.objects.get_for_models(models.Organization, models.Team, models.Credential) # dict of {model: content_type} |
| 4377 | + if not settings.AWX_DIRECT_SHARED_RESOURCE_MANAGEMENT_ENABLED: |
| 4378 | + for model in [models.Organization, models.Team]: |
| 4379 | + ct = content_types[model] |
| 4380 | + if role.content_type == ct and role.role_field in ['member_role', 'admin_role']: |
| 4381 | + data = dict(msg=_(f"Cannot directly modify user membership to {ct.model}. Direct shared resource management disabled")) |
| 4382 | + return Response(data, status=status.HTTP_403_FORBIDDEN) |
| 4383 | + |
| 4384 | + credential_content_type = content_types[models.Credential] |
4299 | 4385 | if role.content_type == credential_content_type:
|
4300 | 4386 | if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
|
4301 | 4387 | data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
|
|
0 commit comments