Skip to content

Commit

Permalink
implemented permission api
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Feb 5, 2024
1 parent 4dc8668 commit 14f16fa
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 86 deletions.
3 changes: 3 additions & 0 deletions src/ansible-webui/aw/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from aw.api_endpoints.key import APIKey, APIKeyItem
from aw.api_endpoints.job import APIJob, APIJobItem, APIJobExecutionItem, APIJobExecutionLogs
from aw.api_endpoints.permission import APIPermission, APIPermissionItem

urlpatterns_api = [
path('api/key/<str:token>', APIKeyItem.as_view()),
Expand All @@ -11,6 +12,8 @@
path('api/job/<int:job_id>/<int:exec_id>', APIJobExecutionItem.as_view()),
path('api/job/<int:job_id>', APIJobItem.as_view()),
path('api/job', APIJob.as_view()),
path('api/setting/permission/<int:perm_id>', APIPermissionItem.as_view()),
path('api/setting/permission', APIPermission.as_view()),
path('api/_schema/', SpectacularAPIView.as_view(), name='_schema'),
path('api/_docs', SpectacularSwaggerView.as_view(url_name='_schema'), name='swagger-ui'),
]
4 changes: 4 additions & 0 deletions src/ansible-webui/aw/api_endpoints/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ def create(self, validated_data):

def update(self, instance, validated_data):
pass


class GenericResponse(BaseResponse):
msg = serializers.CharField()
36 changes: 16 additions & 20 deletions src/ansible-webui/aw/api_endpoints/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

from aw.model.job import Job, JobExecution, BaseJobCredentials, \
CHOICE_JOB_PERMISSION_READ, CHOICE_JOB_PERMISSION_WRITE, CHOICE_JOB_PERMISSION_EXECUTE
from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse
from aw.api_endpoints.job_util import get_viewable_jobs_serialized, job_action_allowed, \
JobReadResponse
from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse, GenericResponse
from aw.api_endpoints.job_util import get_viewable_jobs_serialized, JobReadResponse
from aw.utils.permission import job_action_allowed
from aw.execute.queue import queue_add
from aw.execute.util import update_execution_status, is_execution_status
from aw.utils.util import is_null
Expand All @@ -27,10 +27,6 @@ class Meta:
connect_pass = serializers.CharField(max_length=100, required=False, default=None)


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


def _find_job(job_id: int) -> (Job, None):
# pylint: disable=E1101
return Job.objects.get(id=job_id)
Expand Down Expand Up @@ -60,10 +56,10 @@ def get(request):
return Response(data=get_viewable_jobs_serialized(get_api_user(request)), status=200)

@extend_schema(
request=None,
request=JobWriteRequest,
responses={
200: OpenApiResponse(JobWriteResponse, description='Job created'),
400: OpenApiResponse(JobWriteResponse, description='Invalid job data provided'),
200: OpenApiResponse(GenericResponse, description='Job created'),
400: OpenApiResponse(GenericResponse, description='Invalid job data provided'),
},
summary='Create a new job.',
operation_id='job_create'
Expand Down Expand Up @@ -97,7 +93,7 @@ def post(self, request):

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

@extend_schema(
Expand All @@ -124,9 +120,9 @@ def get(self, request, job_id: int):
@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'),
200: OpenApiResponse(GenericResponse, description='Job deleted'),
403: OpenApiResponse(GenericResponse, description='Not privileged to delete the job'),
404: OpenApiResponse(GenericResponse, description='Job does not exist'),
},
summary='Delete an existing job.',
operation_id='job_delete'
Expand All @@ -149,12 +145,12 @@ def delete(self, request, job_id: int):
return Response(data={'msg': f"Job with ID {job_id} does not exist"}, status=404)

@extend_schema(
request=None,
request=JobWriteRequest,
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'),
200: OpenApiResponse(GenericResponse, description='Job updated'),
400: OpenApiResponse(GenericResponse, description='Invalid job data provided'),
403: OpenApiResponse(GenericResponse, description='Not privileged to modify the job'),
404: OpenApiResponse(GenericResponse, description='Job does not exist'),
},
summary='Modify an existing job.',
operation_id='job_edit'
Expand Down Expand Up @@ -230,7 +226,7 @@ def post(self, request, job_id: int):

class APIJobExecutionItem(APIView):
http_method_names = ['delete']
serializer_class = JobWriteResponse
serializer_class = GenericResponse
permission_classes = API_PERMISSION

@extend_schema(
Expand Down
53 changes: 2 additions & 51 deletions src/ansible-webui/aw/api_endpoints/job_util.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.conf import settings
from rest_framework import serializers

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


class JobReadResponse(serializers.ModelSerializer):
Expand All @@ -11,54 +11,5 @@ class Meta:
fields = Job.api_fields_read


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 not isinstance(job, Job):
raise ValueError(f"Provided job is invalid: '{job}'")

# 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 job_action_allowed(user: settings.AUTH_USER_MODEL, job: Job, permission_needed: int) -> bool:
return get_job_if_allowed(user=user, job=job, permission_needed=permission_needed) is not 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)]
16 changes: 6 additions & 10 deletions src/ansible-webui/aw/api_endpoints/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from aw.utils.util import datetime_w_tz
from aw.config.hardcoded import KEY_TIME_FORMAT
from aw.model.api import AwAPIKey
from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse
from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse, GenericResponse


# todo: allow user to add comment to easier identify token
Expand Down Expand Up @@ -50,28 +50,24 @@ def post(self, request):
return Response({'token': token, 'key': key})


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


class APIKeyItem(APIView):
http_method_names = ['delete']
serializer_class = KeyDeleteResponse
serializer_class = GenericResponse
permission_classes = API_PERMISSION

@extend_schema(
request=None,
responses={
200: OpenApiResponse(response=KeyDeleteResponse, description='API key deleted'),
404: OpenApiResponse(response=KeyDeleteResponse, description='API key does not exist'),
200: OpenApiResponse(response=GenericResponse, description='API key deleted'),
404: OpenApiResponse(response=GenericResponse, 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:
result = AwAPIKey.objects.filter(user=get_api_user(request), name=token)
result = AwAPIKey.objects.get(user=get_api_user(request), name=token)

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

Expand Down
Loading

0 comments on commit 14f16fa

Please sign in to comment.