From 0e816a2d984314cb3c7f0f3a633fb898dfa8954f Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 11 May 2017 13:05:49 -0400 Subject: [PATCH 1/3] Create /inventories/N/update_inventory_sources endpoint --- awx/api/serializers.py | 1 + awx/api/urls.py | 1 + awx/api/views.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index bbdf13d8f0..5871a1dad3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1121,6 +1121,7 @@ class InventorySerializer(BaseSerializerWithVariables): script = self.reverse('api:inventory_script_view', kwargs={'pk': obj.pk}), tree = self.reverse('api:inventory_tree_view', kwargs={'pk': obj.pk}), inventory_sources = self.reverse('api:inventory_inventory_sources_list', kwargs={'pk': obj.pk}), + update_inventory_sources = self.reverse('api:inventory_inventory_sources_update', kwargs={'pk': obj.pk}), activity_stream = self.reverse('api:inventory_activity_stream_list', kwargs={'pk': obj.pk}), job_templates = self.reverse('api:inventory_job_template_list', kwargs={'pk': obj.pk}), scan_job_templates = self.reverse('api:inventory_scan_job_template_list', kwargs={'pk': obj.pk}), diff --git a/awx/api/urls.py b/awx/api/urls.py index 95241638c8..06f4849132 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -93,6 +93,7 @@ inventory_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/script/$', 'inventory_script_view'), url(r'^(?P[0-9]+)/tree/$', 'inventory_tree_view'), url(r'^(?P[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'), + url(r'^(?P[0-9]+)/update_inventory_sources/$', 'inventory_inventory_sources_update'), url(r'^(?P[0-9]+)/activity_stream/$', 'inventory_activity_stream_list'), url(r'^(?P[0-9]+)/job_templates/$', 'inventory_job_template_list'), url(r'^(?P[0-9]+)/scan_job_templates/$', 'inventory_scan_job_template_list'), diff --git a/awx/api/views.py b/awx/api/views.py index fb622cf997..5155d9ceed 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2306,6 +2306,45 @@ class InventoryInventorySourcesList(SubListCreateAPIView): new_in_14 = True +class InventoryInventorySourcesUpdate(RetrieveAPIView): + view_name = _('Inventory Sources Update') + + model = Inventory + serializer_class = InventorySourceUpdateSerializer + is_job_start = True + new_in_320 = True + + def retrieve(self, request, *args, **kwargs): + inventory = self.get_object() + update_data = [] + for inventory_source in inventory.inventory_sources.all(): + details = {'inventory_source': inventory_source.pk, + 'can_update': inventory_source.can_update} + update_data.append(details) + return Response(update_data) + + def post(self, request, *args, **kwargs): + inventory = self.get_object() + update_data = [] + for inventory_source in inventory.inventory_sources.all(): + details = {'inventory_source': inventory_source.pk, 'status': None, 'inventory_update': None} + can_update = inventory_source.can_update + + if inventory_source.source == 'scm' and inventory_source.update_on_project_update: + if not self.request.user or self.request.user.can_access(self.model, 'update', inventory_source): + details['status'] = 'You do not have permission to update project `{}`'.format(inventory_source.source_project.name) + can_update = False + + if can_update: + details['status'] = 'started' + details['inventory_update'] = inventory_source.update().id + else: + if not details.get('status'): + details['status'] = 'Could not start because `can_update` returned False' + update_data.append(details) + return Response(update_data, status=status.HTTP_202_ACCEPTED) + + class InventorySourceList(ListCreateAPIView): model = InventorySource From aff084e1f6717b533499d945700b96d1bc701040 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 11 May 2017 14:01:32 -0400 Subject: [PATCH 2/3] Update permissions/RBAC for updates --- awx/api/permissions.py | 10 ++++++++-- awx/api/views.py | 12 +++++++++--- awx/main/access.py | 4 ++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/awx/api/permissions.py b/awx/api/permissions.py index 966cf95ea5..cf29f3454a 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -16,8 +16,8 @@ from awx.main.utils import get_object_or_400 logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', - 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission', - 'IsSuperUser'] + 'TaskPermission', 'ProjectUpdatePermission', 'InventoryInventorySourcesUpdatePermission', + 'UserPermission', 'IsSuperUser'] class ModelAccessPermission(permissions.BasePermission): @@ -200,6 +200,12 @@ class ProjectUpdatePermission(ModelAccessPermission): return check_user_access(request.user, view.model, 'start', project) +class InventoryInventorySourcesUpdatePermission(ModelAccessPermission): + def check_post_permissions(self, request, view, obj=None): + inventory = get_object_or_400(view.model, pk=view.kwargs['pk']) + return check_user_access(request.user, view.model, 'update', inventory) + + class UserPermission(ModelAccessPermission): def check_post_permissions(self, request, view, obj=None): if not request.data: diff --git a/awx/api/views.py b/awx/api/views.py index 5155d9ceed..06d815b394 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2311,6 +2311,7 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): model = Inventory serializer_class = InventorySourceUpdateSerializer + permission_classes = (InventoryInventorySourcesUpdatePermission,) is_job_start = True new_in_320 = True @@ -2327,15 +2328,20 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): inventory = self.get_object() update_data = [] for inventory_source in inventory.inventory_sources.all(): - details = {'inventory_source': inventory_source.pk, 'status': None, 'inventory_update': None} + details = {'inventory_source': inventory_source.pk, 'status': None} can_update = inventory_source.can_update + project_update = False if inventory_source.source == 'scm' and inventory_source.update_on_project_update: - if not self.request.user or self.request.user.can_access(self.model, 'update', inventory_source): + if not self.request.user or not self.request.user.can_access(Project, 'start', inventory_source.source_project): details['status'] = 'You do not have permission to update project `{}`'.format(inventory_source.source_project.name) can_update = False + else: + project_update = True if can_update: + if project_update: + details['project_update'] = inventory_source.source_project.update().id details['status'] = 'started' details['inventory_update'] = inventory_source.update().id else: @@ -2462,7 +2468,7 @@ class InventorySourceUpdateView(RetrieveAPIView): obj = self.get_object() if obj.can_update: if obj.source == 'scm' and obj.update_on_project_update: - if not self.request.user or self.request.user.can_access(self.model, 'update', obj): + if not self.request.user or not self.request.user.can_access(Project, 'start', obj.source_project): raise PermissionDenied(detail=_( 'You do not have permission to update project `{}`.'.format(obj.source_project.name))) return self._build_update_response(obj.source_project.update(), request) diff --git a/awx/main/access.py b/awx/main/access.py index 4488b09423..78d90d146a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -566,6 +566,10 @@ class InventoryAccess(BaseAccess): # inventory to a new organization. Otherwise, just check for admin permission. return self.check_related('organization', Organization, data, obj=obj) and self.user in obj.admin_role + @check_superuser + def can_update(self, obj): + return self.user in obj.update_role + def can_delete(self, obj): is_can_admin = self.can_admin(obj, None) if not is_can_admin: From 54876e71e7f783309716c05242bfa3b253a747e8 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Thu, 11 May 2017 15:31:39 -0400 Subject: [PATCH 3/3] Added tests for post conditional logic --- awx/api/views.py | 2 +- awx/main/tests/unit/api/test_views.py | 36 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/awx/api/views.py b/awx/api/views.py index 06d815b394..a9e907b417 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2333,7 +2333,7 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): project_update = False if inventory_source.source == 'scm' and inventory_source.update_on_project_update: - if not self.request.user or not self.request.user.can_access(Project, 'start', inventory_source.source_project): + if not request.user or not request.user.can_access(Project, 'start', inventory_source.source_project): details['status'] = 'You do not have permission to update project `{}`'.format(inventory_source.source_project.name) can_update = False else: diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 030150ade3..e4291e50cb 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -7,6 +7,7 @@ from awx.api.views import ( ApiVersionRootView, JobTemplateLabelList, JobTemplateSurveySpec, + InventoryInventorySourcesUpdate, ) @@ -81,3 +82,38 @@ class TestJobTemplateSurveySpec(object): assert response == mock_response_new # which there was a better way to do this! assert response.call_args[0][1]['spec'][0]['default'] == '$encrypted$' + + +class TestInventoryInventorySourcesUpdate: + + @pytest.mark.parametrize("can_update, can_access, is_source, is_up_on_proj, expected", [ + (True, True, "ec2", False, [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), + (False, True, "gce", False, [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]), + (True, False, "scm", True, [{'status': 'You do not have permission to update project `project`', 'inventory_source': 1}]), + ]) + def test_post(self, mocker, can_update, can_access, is_source, is_up_on_proj, expected): + class InventoryUpdate: + id = 1 + + class Project: + name = 'project' + + InventorySource = namedtuple('InventorySource', ['source', 'update_on_project_update', 'pk', 'can_update', + 'update', 'source_project']) + + class InventorySources(object): + def all(self): + return [InventorySource(pk=1, source=is_source, source_project=Project, + update_on_project_update=is_up_on_proj, + can_update=can_update, update=lambda:InventoryUpdate)] + + Inventory = namedtuple('Inventory', ['inventory_sources']) + obj = Inventory(inventory_sources=InventorySources()) + + mock_request = mocker.MagicMock() + mock_request.user.can_access.return_value = can_access + + with mocker.patch.object(InventoryInventorySourcesUpdate, 'get_object', return_value=obj): + view = InventoryInventorySourcesUpdate() + response = view.post(mock_request) + assert response.data == expected