Update UJ/UJT endpoints, update approval RBAC, update approval timeout

This commit is contained in:
beeankha 2019-08-08 16:36:42 -04:00 committed by Ryan Petrello
parent 544a5063f3
commit d9f3fed06f
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
7 changed files with 53 additions and 48 deletions

View File

@ -3414,15 +3414,16 @@ class WorkflowApprovalSerializer(UnifiedJobSerializer):
can_approve_or_deny = serializers.SerializerMethodField()
approval_expiration = serializers.SerializerMethodField()
timed_out = serializers.ReadOnlyField()
class Meta:
model = WorkflowApproval
fields = (['*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration'])
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)
def get_approval_expiration(self, obj):
if obj.status != 'pending' or obj.timeout == 0:
return None
return now() + timedelta(seconds=obj.timeout)
return obj.created + timedelta(seconds=obj.timeout)
def get_can_approve_or_deny(self, obj):
request = self.context.get('request', None)
@ -3442,20 +3443,8 @@ class WorkflowApprovalSerializer(UnifiedJobSerializer):
class WorkflowApprovalListSerializer(WorkflowApprovalSerializer, UnifiedJobListSerializer):
can_approve_or_deny = serializers.SerializerMethodField()
approval_expiration = serializers.SerializerMethodField()
class Meta:
fields = ('*', '-execution_node', '-controller_node', 'can_approve_or_deny', 'approval_expiration')
def get_approval_expiration(self, obj):
if obj.status != 'pending' or obj.timeout == 0:
return None
return now() + timedelta(seconds=obj.timeout)
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
fields = ('*', '-controller_node', '-execution_node', 'can_approve_or_deny', 'approval_expiration', 'timed_out',)
class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer):

View File

@ -4461,7 +4461,7 @@ class WorkflowApprovalApprove(RetrieveAPIView):
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
if obj.status != 'pending':
return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST)
obj.approve()
obj.approve(request)
return Response(status=status.HTTP_204_NO_CONTENT)
@ -4476,7 +4476,7 @@ class WorkflowApprovalDeny(RetrieveAPIView):
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
if obj.status != 'pending':
return Response("This workflow step has already been approved or denied.", status=status.HTTP_400_BAD_REQUEST)
obj.deny()
obj.deny(request)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -2016,7 +2016,7 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess):
if self.user not in cred.use_role:
missing_credentials.append(cred.name)
ujt = node.unified_job_template
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
if ujt and not isinstance(ujt, WorkflowApprovalTemplate) and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
missing_ujt.append(ujt.name)
if missing_ujt:
self.messages['templates_unable_to_copy'] = missing_ujt
@ -2379,13 +2379,17 @@ class UnifiedJobTemplateAccess(BaseAccess):
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'))
) # &&&&&& (filter out approvals from UJT endpoint here...?)
)
def can_start(self, obj, validate_license=True):
access_class = access_registry[obj.__class__]
access_instance = access_class(self.user)
return access_instance.can_start(obj, validate_license=validate_license)
def get_queryset(self):
return super(UnifiedJobTemplateAccess, self).get_queryset().filter(
workflowapprovaltemplate__isnull=True)
class UnifiedJobAccess(BaseAccess):
'''
@ -2429,9 +2433,13 @@ 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)
) # &&&&&& (for filtering out approvals from UJ endpoint...?)
)
return qs
def get_queryset(self):
return super(UnifiedJobAccess, self).get_queryset().filter(
workflowapproval__isnull=True)
class ScheduleAccess(BaseAccess):
'''
@ -2796,10 +2804,6 @@ class WorkflowApprovalAccess(BaseAccess):
unified_job_node__workflow_job__unified_job_template__in=WorkflowJobTemplate.accessible_pk_qs(
self.user, 'read_role'))
def get_queryset(self):
return super(WorkflowApprovalAccess, self).get_queryset().exclude(
workflow_approval_template__isnull=True)
def can_approve_or_deny(self, obj):
if obj.status != 'pending':
return False
@ -2831,10 +2835,6 @@ class WorkflowApprovalTemplateAccess(BaseAccess):
workflowjobtemplatenodes__workflow_job_template__in=WorkflowJobTemplate.accessible_pk_qs(
self.user, 'read_role'))
def get_queryset(self):
return super(WorkflowApprovalTemplateAccess, self).get_queryset().filter(
approvals__isnull=False)
for cls in BaseAccess.__subclasses__():
access_registry[cls.model] = cls

View File

@ -29,7 +29,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='workflowjobtemplate',
name='approval_role',
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['singleton:system_auditor', 'organization.approval_role', 'admin_role'], related_name='+', to='main.Role'),
field=awx.main.fields.ImplicitRoleField(editable=False, null='True', on_delete=django.db.models.deletion.CASCADE, parent_role=['organization.approval_role', 'admin_role'], related_name='+', to='main.Role'),
preserve_default='True',
),
migrations.AlterField(
@ -75,4 +75,9 @@ class Migration(migrations.Migration):
name='timeout',
field=models.IntegerField(blank=True, default=0, help_text='The amount of time (in seconds) before the approval node expires and fails.'),
),
migrations.AddField(
model_name='workflowapproval',
name='timed_out',
field=models.BooleanField(default=False, help_text='Shows when an approval node (with a timeout assigned to it) has timed out.'),
),
]

View File

@ -203,7 +203,6 @@ activity_stream_registrar.connect(WorkflowJobTemplate)
activity_stream_registrar.connect(WorkflowJobTemplateNode)
activity_stream_registrar.connect(WorkflowJob)
activity_stream_registrar.connect(WorkflowApproval)
# activity_stream_registrar.connect(WorkflowApprovalTemplate)
activity_stream_registrar.connect(OAuth2Application)
activity_stream_registrar.connect(OAuth2AccessToken)

View File

@ -2,6 +2,7 @@
# All Rights Reserved.
# Python
import json
import logging
# Django
@ -31,10 +32,12 @@ from awx.main.models.mixins import (
RelatedJobsMixin,
)
from awx.main.models.jobs import LaunchTimeConfigBase, LaunchTimeConfig, JobTemplate
from awx.main.models.activity_stream import ActivityStream
from awx.main.models.credential import Credential
from awx.main.redact import REPLACE_STR
from awx.main.fields import JSONField
from awx.main.utils import schedule_task_manager
from awx.main.utils import model_to_dict, schedule_task_manager
from copy import copy
from urllib.parse import urljoin
@ -397,7 +400,6 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
'approval_role',
])
approval_role = ImplicitRoleField(parent_role=[
'singleton:' + ROLE_SINGLETON_SYSTEM_AUDITOR,
'organization.approval_role', 'admin_role',
])
@ -655,6 +657,11 @@ class WorkflowApproval(UnifiedJob):
default=0,
help_text=_("The amount of time (in seconds) before the approval node expires and fails."),
)
timed_out = models.BooleanField(
default=False,
help_text=_("Shows when an approval node (with a timeout assigned to it) has timed out.")
)
@classmethod
def _get_unified_job_template_class(cls):
@ -671,19 +678,31 @@ class WorkflowApproval(UnifiedJob):
return 'workflow_approval_template'
def approve(self, request=None):
from awx.main.signals import model_serializer_mapping # circular import
self.status = 'successful'
self.save()
changes = model_to_dict(self, model_serializer_mapping())
changes['status']=['pending', 'successful']
ActivityStream(
operation='update',
object1='workflow_approval',
actor=request.user,
changes=json.dumps(changes),
).save()
schedule_task_manager()
return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request)
def deny(self, request=None):
from awx.main.signals import model_serializer_mapping # circular import
self.status = 'failed'
self.save()
changes = model_to_dict(self, model_serializer_mapping())
changes['status']=['pending', 'failed']
ActivityStream(
operation='update',
object1='workflow_approval',
actor=request.user,
changes=json.dumps(changes),
).save()
schedule_task_manager()
return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request)
# &&&&&& Possible placeholder for websocket support
# def websocket_emit_data(self):
# websocket_data = super(WorkflowApproval, self).websocket_emit_data()
# websocket_data.update(dict(project_id=self.project.id)) # ?????
# return websocket_data

View File

@ -519,26 +519,19 @@ class TaskManager():
if not found_acceptable_queue:
logger.debug("{} couldn't be scheduled on graph, waiting for next cycle".format(task.log_format))
def timeout_approval_node(self): # Add websocket stuff for when it transitions to "timed out" (maybe a websocket_emit_status() call)
def timeout_approval_node(self):
workflow_approvals = WorkflowApproval.objects.filter(status='pending')
now = tz_now()
for task in workflow_approvals:
# TODO: copy the timeout to the job itself at launch time, not the template <---- Started to implement steps to do this, but unsure...
approval_timeout_seconds = timedelta(seconds=task.timeout)
if task.timeout == 0:
continue
if (now - task.created) >= approval_timeout_seconds:
logger.info("The approval node {} ({}) has expired after {} seconds.".format(task.name, task.pk, task.timeout))
task.timed_out = True
task.status = 'failed'
task.job_explanation = _("This approval node has timed out.")
task.save(update_fields=['status', 'job_explanation'])
# &&&&&& Placeholder for websocket support
# def detect_pending_approval(self):
# workflow_approvals = WorkflowApproval.objects.filter(status='pending').prefetch_related('workflow_approval_template')
# for task in workflow_approvals:
# if task.status == 'pending':
# workflow_approvals.websocket_emit_status(task.status)
task.save(update_fields=['status', 'job_explanation', 'timed_out'])
def calculate_capacity_consumed(self, tasks):
self.graph = InstanceGroup.objects.capacity_values(tasks=tasks, graph=self.graph)