Move view around and inherit from right view to get OPTIONS

we needed to inherit from GenericAPIView to get the options to render
correctly

q!

add execution env support

add organization validation to the workflowjob

Update awx/api/serializers.py

Co-authored-by: Elijah DeLee <kdelee@redhat.com>

Update awx/api/serializers.py

Co-authored-by: Elijah DeLee <kdelee@redhat.com>
This commit is contained in:
Elijah DeLee
2022-12-21 00:26:49 -05:00
parent 81ba6c0234
commit 02e5ba5f94
6 changed files with 99 additions and 46 deletions

View File

@@ -31,6 +31,7 @@ from django.utils.encoding import force_str
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.timezone import now from django.utils.timezone import now
from django.core.validators import RegexValidator, MaxLengthValidator from django.core.validators import RegexValidator, MaxLengthValidator
from django.db.models import Q
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import ValidationError, PermissionDenied from rest_framework.exceptions import ValidationError, PermissionDenied
@@ -4576,16 +4577,47 @@ class BulkJobNodeSerializer(serializers.Serializer):
class BulkJobLaunchSerializer(serializers.Serializer): class BulkJobLaunchSerializer(serializers.Serializer):
name = serializers.CharField(max_length=512, required=False) # limited by max name of jobs name = serializers.CharField(max_length=512, write_only=True, required=False) # limited by max name of jobs
jobs = BulkJobNodeSerializer(many=True, allow_empty=False, max_length=1000) jobs = BulkJobNodeSerializer(many=True, allow_empty=False, write_only=True, max_length=1000)
description: serializers.CharField(write_only=True, required=False, allow_blank=False)
extra_vars: serializers.CharField(write_only=True, required=False, allow_blank=False)
organization = serializers.PrimaryKeyRelatedField(
queryset=Organization.objects.all(),
required=False,
default=None,
allow_null=True,
help_text=_('Inherit permissions from organization roles.'),
)
# inventory: "", # Here we can use PrimaryKeyRelatedField so it will automagically do rbac/turn into object
limit: serializers.CharField(write_only=True, required=False, allow_blank=False)
scm_branch: serializers.CharField(write_only=True, required=False, allow_blank=False)
# webhook_service: null, # Here we can use PrimaryKeyRelatedField so it will automagically do rbac/turn into object, I think, I'm actually not sure how to use this
# webhook_credential: null, # Here we can use PrimaryKeyRelatedField so it will automagically do rbac/turn into object I think, I'm actually not sure how to use this
skip_tags: serializers.CharField(write_only=True, required=False, allow_blank=False)
job_tags: serializers.CharField(write_only=True, required=False, allow_blank=False)
is_bulk_job: serializers.BooleanField(default=True)
class Meta: class Meta:
fields = ('name', 'jobs') fields = ('name', 'jobs', 'description', 'limit')
read_only_fields = () read_only_fields = ()
def validate(self, attrs): def validate(self, attrs):
request = self.context.get('request', None) request = self.context.get('request', None)
# validate Organization
# - If the orgs is not set, set it to the org of the launching user
# - If the user is part of multiple orgs, throw a validation error saying user is part of multiple orgs, please provide one
if 'organization' not in attrs or attrs['organization'] == None or attrs['oganization'] == '':
if Organization.accessible_pk_qs(request.user, 'read_role').count() == 1:
for tup in Organization.accessible_pk_qs(request.user, 'read_role').all():
attrs['organization'] = tup[0]
elif Organization.accessible_pk_qs(request.user, 'read_role').count() > 1:
raise serializers.ValidationError(_(f"User has permission to multiple Organizations, please set one of them in the request"))
else:
raise serializers.ValidationError(_(f"User not part of any organization, please assign an organization to assign to the bulk job"))
requested_org = {attrs['organization']}
identifiers = set() identifiers = set()
for node in attrs['jobs']: for node in attrs['jobs']:
if 'identifier' in node: if 'identifier' in node:
@@ -4610,12 +4642,15 @@ class BulkJobLaunchSerializer(serializers.Serializer):
[requested_use_labels.add(label) for label in job['labels']] [requested_use_labels.add(label) for label in job['labels']]
if 'instance_groups' in job: if 'instance_groups' in job:
[requested_use_instance_groups.add(instance_group) for instance_group in job['instance_groups']] [requested_use_instance_groups.add(instance_group) for instance_group in job['instance_groups']]
if 'execution_environment' in job:
[requested_use_execution_environments.add(execution_env) for execution_env in job['execution_environment']]
# If we are not a superuser, check we have permissions # If we are not a superuser, check we have permissions
# TODO: As we add other related items, we need to add them here # TODO: As we add other related items, we need to add them here
if request and not request.user.is_superuser: if request and not request.user.is_superuser:
allowed_orgs = set()
if requested_org:
[allowed_orgs.add(tup[0]) for tup in Organization.accessible_pk_qs(request.user, 'read_role').all()]
if requested_org not in allowed_orgs:
ValidationError(_(f"Organization {requested_org} not found"))
allowed_ujts = set() allowed_ujts = set()
[allowed_ujts.add(tup[0]) for tup in UnifiedJobTemplate.accessible_pk_qs(request.user, 'execute_role').all()] [allowed_ujts.add(tup[0]) for tup in UnifiedJobTemplate.accessible_pk_qs(request.user, 'execute_role').all()]
[allowed_ujts.add(tup[0]) for tup in UnifiedJobTemplate.accessible_pk_qs(request.user, 'admin_role').all()] [allowed_ujts.add(tup[0]) for tup in UnifiedJobTemplate.accessible_pk_qs(request.user, 'admin_role').all()]
@@ -4640,7 +4675,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
raise serializers.ValidationError(_(f"Credentials {not_allowed} not found.")) raise serializers.ValidationError(_(f"Credentials {not_allowed} not found."))
if requested_use_labels: if requested_use_labels:
accessible_use_labels = {tup[0] for tup in Label.objects.all()} accessible_use_labels = {tup.id for tup in Label.objects.all()}
if requested_use_labels - accessible_use_labels: if requested_use_labels - accessible_use_labels:
not_allowed = requested_use_labels - accessible_use_labels not_allowed = requested_use_labels - accessible_use_labels
raise serializers.ValidationError(_(f"Labels {not_allowed} not found")) raise serializers.ValidationError(_(f"Labels {not_allowed} not found"))
@@ -4649,14 +4684,21 @@ class BulkJobLaunchSerializer(serializers.Serializer):
# only org admins are allowed to see instance groups # only org admins are allowed to see instance groups
organization_admin_qs = Organization.accessible_pk_qs(request.user, 'admin_role').all() organization_admin_qs = Organization.accessible_pk_qs(request.user, 'admin_role').all()
if organization_admin_qs: if organization_admin_qs:
accessible_use_instance_groups = {tup[0] for tup in InstanceGroup.objects.all()} accessible_use_instance_groups = {tup.id for tup in InstanceGroup.objects.all()}
if requested_use_instance_groups - accessible_use_instance_groups: if requested_use_instance_groups - accessible_use_instance_groups:
not_allowed = requested_use_instance_groups - accessible_use_instance_groups not_allowed = requested_use_instance_groups - accessible_use_instance_groups
raise serializers.ValidationError(_(f"Instance Groups {not_allowed} not found")) raise serializers.ValidationError(_(f"Instance Groups {not_allowed} not found"))
# TODO: Figure out the Execution environment RBAC if requested_use_execution_environments:
# For execution environment, need to figure out the RBAC part. Seems like any user part of an organization can see/use all the execution accessible_execution_env = {
# of that orgnization. So we need to filter out the ee's based on request.user organization. tup.id
for tup in ExecutionEnvironment.objects.filter(
Q(organization__in=Organization.accessible_pk_qs(request.user, 'read_role')) | Q(organization__isnull=True)
).distinct()
}
if requested_use_execution_environments - accessible_execution_env:
not_allowed = requested_use_execution_environments - accessible_execution_env
raise serializers.ValidationError(_(f"Execution Environments {not_allowed} not found"))
# all of the unified job templates and related items have now been checked, we can now grab the objects from the DB # all of the unified job templates and related items have now been checked, we can now grab the objects from the DB
# TODO: As we add more related objects like Label, InstanceGroup, etc we need to add them here # TODO: As we add more related objects like Label, InstanceGroup, etc we need to add them here
@@ -4667,6 +4709,7 @@ class BulkJobLaunchSerializer(serializers.Serializer):
"credentials": {obj.id: obj for obj in Credential.objects.filter(id__in=requested_use_credentials)}, "credentials": {obj.id: obj for obj in Credential.objects.filter(id__in=requested_use_credentials)},
"labels": {obj.id: obj for obj in Label.objects.filter(id__in=requested_use_labels)}, "labels": {obj.id: obj for obj in Label.objects.filter(id__in=requested_use_labels)},
"instance_groups": {obj.id: obj for obj in InstanceGroup.objects.filter(id__in=requested_use_instance_groups)}, "instance_groups": {obj.id: obj for obj in InstanceGroup.objects.filter(id__in=requested_use_instance_groups)},
"execution_environment": {obj.id: obj for obj in ExecutionEnvironment.objects.filter(id__in=requested_use_execution_environments)},
} }
# This loop is generalized so we should only have to add related items to the key_to_obj_map # This loop is generalized so we should only have to add related items to the key_to_obj_map
@@ -4685,6 +4728,9 @@ class BulkJobLaunchSerializer(serializers.Serializer):
objectified_jobs.append(objectified_job) objectified_jobs.append(objectified_job)
attrs['jobs'] = objectified_jobs attrs['jobs'] = objectified_jobs
# map the organization object
for obj in Organization.objects.filter(id__in=requested_org):
attrs['organization'] = obj
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):

View File

@@ -0,0 +1,16 @@
# Bulk Job Launch
This endpoint allows the client to launch multiple UnifiedJobTemplates at a time, along side any launch time parameters that they would normally set at launch time.
Example:
```
{
"name": "my bulk job",
"jobs": [
{"unified_job_template": 7, "inventory": 2},
{"unified_job_template": 7, "credentials": [3]}
]
}
```

View File

@@ -30,6 +30,8 @@ from awx.api.views import (
OAuth2TokenList, OAuth2TokenList,
ApplicationOAuth2TokenList, ApplicationOAuth2TokenList,
OAuth2ApplicationDetail, OAuth2ApplicationDetail,
)
from awx.api.views.bulk import (
BulkJobLaunchView, BulkJobLaunchView,
BulkView, BulkView,
) )

View File

@@ -4301,41 +4301,6 @@ class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
serializer_class = serializers.WorkflowApprovalSerializer serializer_class = serializers.WorkflowApprovalSerializer
from rest_framework.decorators import api_view
class BulkJobLaunchView(APIView):
_ignore_model_permissions = True
permission_classes = [IsAuthenticated]
serializer_class = serializers.BulkJobLaunchSerializer
allowed_methods = ['GET', 'POST', 'OPTIONS']
def get(self, request):
# TODO Return something sensible here, like the defaults
bulkjob_serializer = serializers.BulkJobLaunchSerializer(data={}, context={'request': request})
bulkjob_serializer.is_valid()
return Response(bulkjob_serializer.errors, status=status.HTTP_200_OK)
def post(self, request):
bulkjob_serializer = serializers.BulkJobLaunchSerializer(data=request.data, context={'request': request})
if bulkjob_serializer.is_valid():
result = bulkjob_serializer.create(bulkjob_serializer.validated_data)
return Response(result, status=status.HTTP_201_CREATED)
return Response(bulkjob_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BulkView(APIView):
_ignore_model_permissions = True
permission_classes = [IsAuthenticated]
allowed_methods = ['GET']
def get(self, request, format=None):
'''List top level resources'''
data = OrderedDict()
data['bulk_job_launch'] = reverse('api:bulk_job_launch', request=request)
return Response(data)
class WorkflowApprovalApprove(RetrieveAPIView): class WorkflowApprovalApprove(RetrieveAPIView):
model = models.WorkflowApproval model = models.WorkflowApproval
serializer_class = serializers.WorkflowApprovalViewSerializer serializer_class = serializers.WorkflowApprovalViewSerializer

View File

@@ -16,6 +16,25 @@ from awx.api import (
) )
class BulkJobLaunchView(GenericAPIView):
_ignore_model_permissions = True
permission_classes = [IsAuthenticated]
serializer_class = serializers.BulkJobLaunchSerializer
allowed_methods = ['GET', 'POST', 'OPTIONS']
def get(self, request):
data = OrderedDict()
data['detail'] = "Specify a list of unified job templates to launch alongside their launchtime parameters"
return Response(data, status=status.HTTP_200_OK)
def post(self, request):
bulkjob_serializer = serializers.BulkJobLaunchSerializer(data=request.data, context={'request': request})
if bulkjob_serializer.is_valid():
result = bulkjob_serializer.create(bulkjob_serializer.validated_data)
return Response(result, status=status.HTTP_201_CREATED)
return Response(bulkjob_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BulkView(APIView): class BulkView(APIView):
_ignore_model_permissions = True _ignore_model_permissions = True
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -29,6 +48,7 @@ class BulkView(APIView):
'''List top level resources''' '''List top level resources'''
data = OrderedDict() data = OrderedDict()
data['bulk_host_create'] = reverse('api:bulk_host_create', request=request) data['bulk_host_create'] = reverse('api:bulk_host_create', request=request)
data['bulk_job_launch'] = reverse('api:bulk_job_launch', request=request)
return Response(data) return Response(data)

View File

@@ -2123,7 +2123,11 @@ class WorkflowJobAccess(BaseAccess):
) )
def filtered_queryset(self): def filtered_queryset(self):
return WorkflowJob.objects.filter(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) return WorkflowJob.objects.filter(
Q(unified_job_template__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'))
| Q(created_by__in=str(self.user.id), is_bulk_job=True)
| Q(organization__in=Organization.objects.filter(Q(admin_role__members=self.user)), is_bulk_job=True)
)
def can_add(self, data): def can_add(self, data):
# Old add-start system for launching jobs is being depreciated, and # Old add-start system for launching jobs is being depreciated, and