diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c73a4148cf..e494181603 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3410,9 +3410,16 @@ class WorkflowApprovalViewSerializer(UnifiedJobSerializer): class WorkflowApprovalSerializer(UnifiedJobSerializer): + can_approve_or_deny = serializers.SerializerMethodField() + class Meta: model = WorkflowApproval - fields = (['*', '-controller_node', '-execution_node',]) + fields = (['*', '-controller_node', '-execution_node', 'can_approve_or_deny']) + + def get_can_approve_or_deny(self, obj): + request = self.context.get('request', None) + allowed = request.user.can_access(WorkflowApproval, 'approve_or_deny', obj) + return allowed is True and obj.status == 'pending' def get_related(self, obj): res = super(WorkflowApprovalSerializer, self).get_related(obj) @@ -3420,17 +3427,21 @@ class WorkflowApprovalSerializer(UnifiedJobSerializer): if obj.workflow_approval_template: res['workflow_approval_template'] = self.reverse('api:workflow_approval_template_detail', kwargs={'pk': obj.workflow_approval_template.pk}) - res['notifications'] = self.reverse('api:workflow_approval_notifications_list', kwargs={'pk': obj.pk}) res['approve'] = self.reverse('api:workflow_approval_approve', kwargs={'pk': obj.pk}) res['deny'] = self.reverse('api:workflow_approval_deny', kwargs={'pk': obj.pk}) return res - class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer): + can_approve_or_deny = serializers.SerializerMethodField() + class Meta: - fields = ('*', '-execution_node', '-controller_node',) + fields = ('*', '-execution_node', '-controller_node', 'can_approve_or_deny') + + def get_can_approve_or_deny(self, obj): + request = self.context.get('request', None) + return request.user.can_access(WorkflowApproval, 'approve_or_deny', obj) is True class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): @@ -3446,13 +3457,7 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): res.update(dict( jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}), - # &&&&&& Placeholder for notification things! - # notification_templates_started = self.reverse('api:workflow_approval_template_notification_templates_started_list', kwargs={'pk': obj.pk}), - # notification_templates_needs_approval = self.reverse( - #'api:workflow_approval_template_notification_templates_needs_approval_list', kwargs={'pk': obj.pk}), - # notification_templates_success = self.reverse('api:workflow_approval_template_notification_templates_success_list', kwargs={'pk': obj.pk}), - # notification_templates_error = self.reverse('api:workflow_approval_template_notification_templates_error_list', kwargs={'pk': obj.pk}), - )) + )) return res diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index a2a4bc3daa..2215c14daa 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -4440,8 +4440,6 @@ class WorkflowApprovalList(ListCreateAPIView): serializer_class = serializers.WorkflowApprovalListSerializer def get(self, request, *args, **kwargs): - if not request.user.is_superuser and not request.user.is_system_auditor: - raise PermissionDenied(_("Superuser privileges needed.")) return super(WorkflowApprovalList, self).get(request, *args, **kwargs) @@ -4455,22 +4453,29 @@ class WorkflowApprovalApprove(RetrieveAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalViewSerializer - # &&&&&& To address later + # &&&&&& Changed per the PR review, notes/questions in additional comments... def post(self, request, *args, **kwargs): obj = self.get_object() + request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj) + if obj.status != 'pending': + return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST) obj.approve() - return Response(status=status.HTTP_202_ACCEPTED) + return Response(status=status.HTTP_204_NO_CONTENT) class WorkflowApprovalDeny(RetrieveAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalViewSerializer - # &&&&&& To address later + # &&&&&& Changed per the PR review, notes/questions in additional comments... def post(self, request, *args, **kwargs): obj = self.get_object() + request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj) + if obj.status != 'pending': + return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST) obj.deny() - return Response(status=status.HTTP_202_ACCEPTED) + return Response(status=status.HTTP_204_NO_CONTENT) + class WorkflowApprovalNotificationsList(SubListAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index 65fe0badd5..ca0ccebf93 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2378,7 +2378,8 @@ class UnifiedJobTemplateAccess(BaseAccess): return self.model.objects.filter( Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')) | Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs( - Inventory, self.user, 'read_role'))) + Inventory, self.user, 'read_role')) + ).exclude(polymorphic_ctype__model='workflowapprovaltemplate') # &&&&&& def can_start(self, obj, validate_license=True): access_class = access_registry[obj.__class__] @@ -2428,7 +2429,7 @@ class UnifiedJobAccess(BaseAccess): Q(adhoccommand__inventory__id__in=inv_pk_qs) | Q(job__inventory__organization__in=org_auditor_qs) | Q(job__project__organization__in=org_auditor_qs) - ) + ).exclude(polymorphic_ctype__model='workflowapproval') # &&&&&& return qs @@ -2793,7 +2794,7 @@ class WorkflowApprovalAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - unified_job_node__in=WorkflowJobNode.accessible_pk_qs( + unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs( self.user, 'read_role')) def get_queryset(self): @@ -2801,7 +2802,8 @@ class WorkflowApprovalAccess(BaseAccess): workflow_approval_template__isnull=False) def can_approve_or_deny(self, obj): - if self.user.approval_role or self.user.system_administrator: + wfjt = obj.unified_job_node.workflow_job.unified_job_template + if self.user in wfjt.approval_role or self.user.is_superuser: return True diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index b79abbad0d..3d8b6d1734 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -482,6 +482,10 @@ class TaskManager(): found_acceptable_queue = False idle_instance_that_fits = None if isinstance(task, WorkflowJob): + # &&&&&& Timeout implementation (pseudo-code) + # if (tz_now() - task.created).seconds > the_timeout_in_seconds: + # logger.error('meaning log message') + # mark it as status failed and set a reasonable `job_explanation` value if task.unified_job_template_id in running_workflow_templates: if not task.allow_simultaneous: logger.debug("{} is blocked from running, workflow already running".format(task.log_format)) diff --git a/awx/main/signals.py b/awx/main/signals.py index 34658473f7..522aa1b248 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -505,6 +505,10 @@ def activity_stream_update(sender, instance, **kwargs): else: activity_entry.setting = conf_to_dict(instance) activity_entry.save() + # &&&&&& + # for approvals in kwargs['pk_set']: + # if isinstance(WorkflowApprovalTemplate) or isinstance(kwargs['model'].objects.filter(id=approvals), WorkflowApprovalTemplate): + # continue def activity_stream_delete(sender, instance, **kwargs): @@ -641,14 +645,14 @@ def delete_inventory_for_org(sender, instance, **kwargs): @receiver(pre_delete, sender=WorkflowJobTemplateNode) -def delete_approval_nodes(sender, instance, **kwargs): +def delete_approval_templates(sender, instance, **kwargs): if type(instance.unified_job_template) is WorkflowApprovalTemplate: instance.unified_job_template.delete() -# When setting UJT to anything other than "is approval node" - update this comment! +# When setting UJT to anything other than "is approval node" - delete this comment! @receiver(pre_save, sender=WorkflowJobTemplateNode) -def placeholder_name(sender, instance, **kwargs): +def delete_approval_node_type_change(sender, instance, **kwargs): try: old = WorkflowJobTemplateNode.objects.get(id=instance.id) except sender.DoesNotExist: @@ -659,6 +663,13 @@ def placeholder_name(sender, instance, **kwargs): old.unified_job_template.delete() +# &&&&&& New stuff to test! +@receiver(post_delete, sender=WorkflowApprovalTemplate) +def deny_orphaned_approvals(sender, instance, **kwargs): + for approval in WorkflowApproval.objects.filter(workflow_approval_template=instance, status='pending'): + approval.deny() + + @receiver(post_save, sender=Session) def save_user_session_membership(sender, **kwargs): session = kwargs.get('instance', None)