From 1a26a1796bf1572942992649526a7a43eec81ccf Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 19 Dec 2016 11:02:40 -0500 Subject: [PATCH 1/4] break WJ relaunch check from start capability implement special custom error message for Workflow Job relaunch ability corner case --- awx/api/views.py | 7 +++ awx/main/access.py | 90 ++++++++++++++++++++++++++++--------- awx/main/models/workflow.py | 3 ++ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index f19b61e4e9..dc49d55e64 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2953,6 +2953,13 @@ class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView): serializer_class = EmptySerializer is_job_start = True + def check_object_permissions(self, request, obj): + if request.method == 'POST' and obj: + relaunch_perm, messages = request.user.can_access_with_errors(self.model, 'start', obj) + if not relaunch_perm: + self.permission_denied(request, message=messages['workflow_job_template']) + return super(WorkflowJobRelaunch, self).check_object_permissions(request, obj) + def get(self, request, *args, **kwargs): return Response({}) diff --git a/awx/main/access.py b/awx/main/access.py index 422ed6be7c..9054ab7b94 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -363,27 +363,30 @@ class BaseAccess(object): continue # Compute permission - data = {} - access_method = getattr(self, "can_%s" % method) - if method in ['change']: # 3 args - user_capabilities[display_method] = access_method(obj, data) - elif method in ['delete', 'run_ad_hoc_commands', 'copy']: - user_capabilities[display_method] = access_method(obj) - elif method in ['start']: - user_capabilities[display_method] = access_method(obj, validate_license=False) - elif method in ['add']: # 2 args with data - user_capabilities[display_method] = access_method(data) - elif method in ['attach', 'unattach']: # parent/sub-object call - if type(parent_obj) == Team: - relationship = 'parents' - parent_obj = parent_obj.member_role - else: - relationship = 'members' - user_capabilities[display_method] = access_method( - obj, parent_obj, relationship, skip_sub_obj_read_check=True, data=data) + user_capabilities[display_method] = self.get_method_capability(method, obj, parent_obj) return user_capabilities + def get_method_capability(self, method, obj, parent_obj): + if method in ['change']: # 3 args + return self.can_change(obj, {}) + elif method in ['delete', 'run_ad_hoc_commands', 'copy']: + access_method = getattr(self, "can_%s" % method) + return access_method(obj) + elif method in ['start']: + return self.can_start(obj, validate_license=False) + elif method in ['add']: # 2 args with data + return self.can_add({}) + elif method in ['attach', 'unattach']: # parent/sub-object call + access_method = getattr(self, "can_%s" % method) + if type(parent_obj) == Team: + relationship = 'parents' + parent_obj = parent_obj.member_role + else: + relationship = 'members' + return access_method(obj, parent_obj, relationship, skip_sub_obj_read_check=True, data={}) + return False + class UserAccess(BaseAccess): ''' @@ -1294,6 +1297,12 @@ class JobAccess(BaseAccess): def can_delete(self, obj): return self.org_access(obj) + def get_method_capability(self, method, obj, parent_obj): + if method == 'start': + # Return simplistic permission, will perform detailed check on POST + return (not obj.job_template) or self.user in obj.job_template.execute_role + return super(JobAccess, self).get_method_capability(method, obj, parent_obj) + def can_start(self, obj, validate_license=True): if validate_license: self.check_license() @@ -1483,8 +1492,14 @@ class WorkflowJobNodeAccess(BaseAccess): qs = qs.prefetch_related('success_nodes', 'failure_nodes', 'always_nodes') return qs + @check_superuser def can_add(self, data): - return False + if data is None: # Hide direct creation in API browser + return False + return ( + self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role') and + self.check_related('credential', Credential, data, role_field='use_role') and + self.check_related('inventory', Inventory, data, role_field='use_role')) def can_change(self, obj, data): return False @@ -1622,6 +1637,14 @@ class WorkflowJobAccess(BaseAccess): return self.user.is_superuser return self.user in obj.workflow_job_template.admin_role + def get_method_capability(self, method, obj, parent_obj): + if method == 'start': + # Return simplistic permission, will perform detailed check on POST + if not obj.workflow_job_template: + return self.user.is_superuser + return self.user in obj.workflow_job_template.execute_role + return super(WorkflowJobAccess, self).get_method_capability(method, obj, parent_obj) + def can_start(self, obj, validate_license=True): if validate_license: self.check_license() @@ -1629,7 +1652,34 @@ class WorkflowJobAccess(BaseAccess): if self.user.is_superuser: return True - return (obj.workflow_job_template and self.user in obj.workflow_job_template.execute_role) + wfjt = obj.workflow_job_template + # only superusers can relaunch orphans + if not wfjt: + return False + + # execute permission to WFJT is mandatory for any relaunch + if self.user not in wfjt.execute_role: + return False + + # WFJT is valid base for WJ, launch permitted + last_modified = wfjt.nodes_last_modified() + if last_modified and obj.created > last_modified: + return True + + # user's WFJT access doesn't guarentee permission to launch, introspect nodes + return self.can_readd(obj) + + def can_readd(self, obj): + node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credential', 'unified_job_template') + node_access = WorkflowJobNodeAccess(user=self.user) + wj_add_perm = True + for node in node_qs: + if not node_access.can_add({'reference_obj': node}): + wj_add_perm = False + if not wj_add_perm and self.save_messages: + self.messages['workflow_job_template'] = ('Template has been modified since job was launched, ' + 'and you do not have permission to its resources.') + return wj_add_perm def can_cancel(self, obj): if not obj.can_cancel: diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 526f0a0300..09870cbc18 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -327,6 +327,9 @@ class WorkflowJobOptions(BaseModel): new_workflow_job.copy_nodes_from_original(original=self) return new_workflow_job + def nodes_last_modified(self): + return self.workflow_nodes.aggregate(models.Max('modified'))['modified__max'] + class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin): class Meta: From 93564987d1ecca6e9aa4c2a0a0180b7558115655 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 20 Dec 2016 08:18:13 -0500 Subject: [PATCH 2/4] hold off on using new capabilities methodology on jobs --- awx/api/views.py | 2 +- awx/main/access.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index dc49d55e64..219c48ce4e 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2956,7 +2956,7 @@ class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView): def check_object_permissions(self, request, obj): if request.method == 'POST' and obj: relaunch_perm, messages = request.user.can_access_with_errors(self.model, 'start', obj) - if not relaunch_perm: + if not relaunch_perm and 'workflow_job_template' in messages: self.permission_denied(request, message=messages['workflow_job_template']) return super(WorkflowJobRelaunch, self).check_object_permissions(request, obj) diff --git a/awx/main/access.py b/awx/main/access.py index 9054ab7b94..f294aa6cda 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1297,12 +1297,6 @@ class JobAccess(BaseAccess): def can_delete(self, obj): return self.org_access(obj) - def get_method_capability(self, method, obj, parent_obj): - if method == 'start': - # Return simplistic permission, will perform detailed check on POST - return (not obj.job_template) or self.user in obj.job_template.execute_role - return super(JobAccess, self).get_method_capability(method, obj, parent_obj) - def can_start(self, obj, validate_license=True): if validate_license: self.check_license() From 7acb89ff4a006fa5e15a91b4697d0551c74912aa Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 4 Jan 2017 09:21:50 -0500 Subject: [PATCH 3/4] wrap error message in internationalization marker --- awx/main/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index f294aa6cda..70ac423098 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1671,8 +1671,8 @@ class WorkflowJobAccess(BaseAccess): if not node_access.can_add({'reference_obj': node}): wj_add_perm = False if not wj_add_perm and self.save_messages: - self.messages['workflow_job_template'] = ('Template has been modified since job was launched, ' - 'and you do not have permission to its resources.') + self.messages['workflow_job_template'] = _('Template has been modified since job was launched, ' + 'and you do not have permission to its resources.') return wj_add_perm def can_cancel(self, obj): From 5fcd467fe6ac8b48e49c675da10d004255e789d9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 6 Jan 2017 14:44:29 -0500 Subject: [PATCH 4/4] rename helper function for WJ relaunch --- awx/main/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 70ac423098..89d04586c2 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1661,9 +1661,9 @@ class WorkflowJobAccess(BaseAccess): return True # user's WFJT access doesn't guarentee permission to launch, introspect nodes - return self.can_readd(obj) + return self.can_recreate(obj) - def can_readd(self, obj): + def can_recreate(self, obj): node_qs = obj.workflow_job_nodes.all().prefetch_related('inventory', 'credential', 'unified_job_template') node_access = WorkflowJobNodeAccess(user=self.user) wj_add_perm = True