Skip to content

Commit

Permalink
prepared jobs-api
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Jan 20, 2024
1 parent 87ef152 commit fa910c4
Show file tree
Hide file tree
Showing 26 changed files with 593 additions and 97 deletions.
5 changes: 4 additions & 1 deletion src/ansible-webui/aw/admin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from django.contrib import admin

from aw.models import Job, JobExecution, JobPermission, JobPermissionMemberUser, JobPermissionMemberGroup, AwAPIKey
from aw.models import Job, JobExecution, \
JobPermission, JobPermissionMemberUser, JobPermissionMemberGroup, JobPermissionMapping, \
AwAPIKey

admin.site.register(Job)
admin.site.register(JobExecution)
admin.site.register(JobPermission)
admin.site.register(JobPermissionMemberUser)
admin.site.register(JobPermissionMemberGroup)
admin.site.register(JobPermissionMapping)
admin.site.register(AwAPIKey)
3 changes: 3 additions & 0 deletions src/ansible-webui/aw/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

from aw.api_endpoints.key import KeyReadWrite, KeyDelete
from aw.api_endpoints.job import JobListCreate, JobViewEditDelete

urlpatterns_api = [
path('api/key', KeyReadWrite.as_view()),
path('api/key/<str:token>', KeyDelete.as_view()),
path('api/job', JobListCreate.as_view()),
path('api/job/<int:job_id>', JobViewEditDelete.as_view()),
path('api/_schema/', SpectacularAPIView.as_view(), name='_schema'),
path('api/_docs', SpectacularSwaggerView.as_view(url_name='_schema'), name='swagger-ui'),
]
1 change: 0 additions & 1 deletion src/ansible-webui/aw/api_endpoints/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def get_api_user(request) -> settings.AUTH_USER_MODEL:


class BaseResponse(serializers.Serializer):

def create(self, validated_data):
pass

Expand Down
144 changes: 144 additions & 0 deletions src/ansible-webui/aw/api_endpoints/job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiResponse

from aw.models import Job
from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse
from aw.api_endpoints.job_util import get_viewable_jobs_serialized, get_job_if_allowed, \
JobReadResponse
from aw.model.job import CHOICE_JOB_PERMISSION_READ, CHOICE_JOB_PERMISSION_WRITE


class JobWriteRequest(JobReadResponse):
pass


class JobWriteResponse(BaseResponse):
msg = serializers.CharField()


class JobListCreate(APIView):
http_method_names = ['post', 'get']
serializer_class = JobReadResponse
permission_classes = API_PERMISSION

@staticmethod
@extend_schema(
request=None,
responses={
200: OpenApiResponse(JobReadResponse, description='Return list of jobs'),
},
summary='Return list of all jobs the current user is privileged to view.',
)
def get(request):
return Response(data=get_viewable_jobs_serialized(get_api_user(request)), status=200)

@extend_schema(
request=None,
responses={
200: OpenApiResponse(JobWriteResponse, description='Job created'),
400: OpenApiResponse(JobWriteResponse, description='Invalid job data provided'),
},
summary='Create a new job.',
)
def post(self, request):
serializer = JobWriteRequest(data=request.data)
if not serializer.is_valid():
return Response(data={'msg': 'Provided job data is not valid'}, status=400)

serializer.save()
return Response(data={'msg': 'Job created'}, status=200)


class JobViewEditDelete(APIView):
http_method_names = ['get', 'delete', 'put']
serializer_class = JobWriteResponse
permission_classes = API_PERMISSION

@staticmethod
@extend_schema(
request=None,
responses={
200: OpenApiResponse(JobReadResponse, description='Return job information'),
404: OpenApiResponse(JobReadResponse, description='Job does not exist'),
},
summary='Return information about an existing job.',
)
def get(request, job_id: int):
# pylint: disable=E1101
user = get_api_user(request)
job = get_job_if_allowed(
user=user,
job=Job.objects.filter(id=job_id),
permission_needed=CHOICE_JOB_PERMISSION_READ,
)
if job is None:
return Response(data={'msg': 'Job does not exist or is not viewable'}, status=404)

return Response(data=JobReadResponse(instance=job).data, status=200)

@extend_schema(
request=None,
responses={
200: OpenApiResponse(JobWriteResponse, description='Job deleted'),
403: OpenApiResponse(JobWriteResponse, description='Not privileged to delete the job'),
404: OpenApiResponse(JobWriteResponse, description='Job does not exist'),
},
summary='Delete an existing job.',
)
def delete(self, request, job_id: int):
# pylint: disable=E1101
user = get_api_user(request)
try:
result = Job.objects.filter(id=job_id)

if result.exists():
result = get_job_if_allowed(user=user, job=result, permission_needed=CHOICE_JOB_PERMISSION_WRITE)

if result is not None:
result.delete()
return Response(data={'msg': 'Job deleted'}, status=200)

return Response(data={'msg': 'Not privileged to delete the job'}, status=403)

except ObjectDoesNotExist:
pass

return Response(data={'msg': 'Job does not exist'}, status=404)

@extend_schema(
request=None,
responses={
200: OpenApiResponse(JobWriteResponse, description='Job updated'),
400: OpenApiResponse(JobWriteResponse, description='Invalid job data provided'),
403: OpenApiResponse(JobWriteResponse, description='Not privileged to modify the job'),
404: OpenApiResponse(JobWriteResponse, description='Job does not exist'),
},
summary='Delete an existing job.',
)
def put(self, request, job_id: int):
# pylint: disable=E1101
user = get_api_user(request)
try:
result = Job.objects.filter(id=job_id)

if result.exists():
result = get_job_if_allowed(user=user, job=result, permission_needed=CHOICE_JOB_PERMISSION_WRITE)

if result is None:
return Response(data={'msg': 'Not privileged to modify the job'}, status=403)

serializer = JobWriteRequest(data=request.data)
if not serializer.is_valid():
return Response(data={'msg': 'Provided job data is not valid'}, status=400)

result.update(**serializer.data)
result.save()
return Response(data={'msg': 'Job updated'}, status=200)

except ObjectDoesNotExist:
pass

return Response(data={'msg': 'Job does not exist'}, status=404)
57 changes: 57 additions & 0 deletions src/ansible-webui/aw/api_endpoints/job_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.conf import settings
from rest_framework import serializers

from aw.model.job import Job, JobPermissionMapping, JobPermissionMemberUser, JobPermissionMemberGroup, \
CHOICE_JOB_PERMISSION_READ


class JobReadResponse(serializers.ModelSerializer):
class Meta:
model = Job
fields = Job.api_fields


def get_job_if_allowed(user: settings.AUTH_USER_MODEL, job: Job, permission_needed: int) -> (Job, None):
# pylint: disable=E1101
if job is None:
return None

# if job has no permissions set
permission_links = JobPermissionMapping.objects.filter(job=job)
if not permission_links.exists():
return job

for link in permission_links:
# ignore permission if access-level is too low
if link.permission < permission_needed:
continue

# if one of the permissions allows the user
if JobPermissionMemberUser.objects.filter(user=user, permission=link.permission).exists():
return job

# if one of the permissions allows a group that the user is a member of
groups = JobPermissionMemberGroup.objects.filter(permission=link.permission)
if groups.exists() and user.groups.filter(name__in=[
group.name for group in groups
]).exists():
return job

return None


def get_viewable_jobs(user: settings.AUTH_USER_MODEL) -> list[Job]:
# pylint: disable=E1101
jobs = Job.objects.all()
jobs_viewable = []

for job in jobs:
job = get_job_if_allowed(user=user, job=job, permission_needed=CHOICE_JOB_PERMISSION_READ)
if job is not None:
jobs_viewable.append(job)

return jobs_viewable


def get_viewable_jobs_serialized(user: settings.AUTH_USER_MODEL) -> list[dict]:
return [JobReadResponse(instance=job).data for job in get_viewable_jobs(user)]
33 changes: 11 additions & 22 deletions src/ansible-webui/aw/api_endpoints/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,27 @@ class KeyDeleteResponse(BaseResponse):


class KeyDelete(APIView):
http_method_names = ['post', 'delete']
http_method_names = ['delete']
serializer_class = KeyDeleteResponse
permission_classes = API_PERMISSION
_schema_responses = {
200: OpenApiResponse(response=KeyDeleteResponse, description='API key deleted'),
404: OpenApiResponse(response=KeyDeleteResponse, description='API key does not exist'),
}
_schema_summary = 'Delete one of the existing API keys of the current user.'

@extend_schema(
request=None,
responses=_schema_responses,
summary=_schema_summary,
responses={
200: OpenApiResponse(response=KeyDeleteResponse, description='API key deleted'),
404: OpenApiResponse(response=KeyDeleteResponse, description='API key does not exist'),
},
summary='Delete one of the existing API keys of the current user.',
)
def delete(self, request, token: str):
try:
results = AwAPIKey.objects.filter(user=get_api_user(request), name=token)
result = AwAPIKey.objects.filter(user=get_api_user(request), name=token)

if len(results) == 1:
results.delete()
return Response({'msg': f"API key '{token}' deleted"})
if result.exists():
result.delete()
return Response(data={'msg': 'API key deleted'}, status=200)

except ObjectDoesNotExist:
pass

return Response(data={'msg': f"API key '{token}' not found"}, status=404)

@extend_schema(
request=None,
responses=_schema_responses,
summary=_schema_summary,
operation_id='del',
)
def post(self, request, token: str):
return self.delete(request, token)
return Response(data={'msg': 'API key not found'}, status=404)
30 changes: 30 additions & 0 deletions src/ansible-webui/aw/config/form_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FORM_LABEL = {
'jobs': {
'manage': {
'job': {
'environment_vars': 'Environmental Variables',
}
}
}
}

FORM_HELP = {
'jobs': {
'manage': {
'job': {
'inventory': 'One or multiple inventory files/directories to include for the execution. '
'Comma-separated list. For details see: '
'<a href="https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html">'
'Ansible Docs - Inventory</a>',
'limit': 'Ansible inventory hosts or groups to limit the execution to.'
'For details see: '
'<a href="https://docs.ansible.com/ansible/latest/inventory_guide/intro_patterns.html">'
'Ansible Docs - Limit</a>',
'schedule': 'Schedule for running the job automatically. For format see: '
'<a href="https://crontab.guru/">crontab.guru</a>',
'environment_vars': 'Environmental variables to be passed to the Ansible execution. '
'Comma-separated list of key-value pairs. (VAR1=TEST1,VAR2=0)',
}
}
}
}
4 changes: 2 additions & 2 deletions src/ansible-webui/aw/config/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
'left': {
'Dashboard': '/ui/',
'Jobs': {
'Manage': '/ui/job/manage/',
'Logs': '/ui/job/log/',
'Manage': '/ui/jobs/manage',
'Logs': '/ui/jobs/log',
},
'Settings': {
'API Keys': '/ui/settings/api_keys',
Expand Down
Loading

0 comments on commit fa910c4

Please sign in to comment.