From 9366b47346c35cb95c39edc7eb1599d4c46f0684 Mon Sep 17 00:00:00 2001 From: Erwan Rouchet <rouchet@teklia.com> Date: Fri, 31 Mar 2023 13:36:46 +0200 Subject: [PATCH] Rewrite CreateArtifact permissions --- arkindex/ponos/api.py | 15 +++++------- arkindex/ponos/permissions.py | 32 ++++++++++++++++++++++++ arkindex/ponos/tests/test_api.py | 42 ++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/arkindex/ponos/api.py b/arkindex/ponos/api.py index 79593e4ac5..1f6010881b 100644 --- a/arkindex/ponos/api.py +++ b/arkindex/ponos/api.py @@ -28,6 +28,7 @@ from arkindex.ponos.permissions import ( IsAgentOrArtifactAdmin, IsAgentOrTask, IsAgentOrTaskAdmin, + IsAgentOrTaskAdminOrReadOnly, IsAssignedAgentOrReadOnly, ) from arkindex.ponos.recipe import parse_recipe @@ -49,7 +50,7 @@ from arkindex.ponos.serializers import ( ) from arkindex.process.models import Process from arkindex.project.mixins import ProcessACLMixin -from arkindex.project.permissions import IsAuthenticated, IsVerified +from arkindex.project.permissions import IsVerified from arkindex.users.models import Role from rest_framework_simplejwt.views import TokenRefreshView @@ -314,26 +315,22 @@ class TaskArtifacts(ListCreateAPIView): # Used for OpenAPI schema serialization: the ID in the path is the task ID queryset = Task.objects.none() - permission_classes = (IsAuthenticated, ) + permission_classes = (IsAgentOrTaskAdminOrReadOnly, ) serializer_class = ArtifactSerializer # Force no pagination, even when global settings add them pagination_class = None def get_task(self): - if "pk" not in self.kwargs: - return Artifact.objects.none() - return get_object_or_404(Task, pk=self.kwargs["pk"]) + task = get_object_or_404(Task, pk=self.kwargs["pk"]) + self.check_object_permissions(self.request, task) + return task def get_queryset(self): task = self.get_task() return task.artifacts.all() def perform_create(self, serializer): - user = self.request.user - if not user.is_staff and not getattr(user, "is_agent", False): - raise PermissionDenied() - # Assign task when creating through the API serializer.save(task=self.get_task()) diff --git a/arkindex/ponos/permissions.py b/arkindex/ponos/permissions.py index 0dcdb04f6f..d059cd2d37 100644 --- a/arkindex/ponos/permissions.py +++ b/arkindex/ponos/permissions.py @@ -3,6 +3,7 @@ from rest_framework.permissions import SAFE_METHODS from arkindex.ponos.models import Task from arkindex.project.mixins import CorpusACLMixin from arkindex.project.permissions import IsAuthenticated +from arkindex.users.models import Role def require_agent_or_admin(request, view): @@ -74,6 +75,37 @@ class IsAgentOrTaskAdmin(CorpusACLMixin, IsAuthenticated): ) +class IsAgentOrTaskAdminOrReadOnly(CorpusACLMixin, IsAuthenticated): + """ + Instance admins, agents, process admins, and the task itself are always allowed. + For GET/HEAD, only a Guest level on the process is required for regular users. + """ + + def has_object_permission(self, request, view, task): + # Allow agents and instance admins + if require_agent_or_admin(request, view): + return True + + # Allow a task to access itself + if isinstance(request.auth, Task) and request.auth.id == task.id: + return True + + # Add request to attributes for the ACL mixin to work with self.user + self.request = request + level = self.process_access_level(task.workflow.process) + + # Require *some* access to the process + if level is None: + return False + + # Require only a guest access for GET/HEAD + if request.method in SAFE_METHODS: + return level >= Role.Guest.value + + # Require admin access for other methods + return level >= Role.Admin.value + + class IsAgentOrArtifactAdmin(IsAgentOrTaskAdmin): """ Permission to access an artifact with high privilege, based on diff --git a/arkindex/ponos/tests/test_api.py b/arkindex/ponos/tests/test_api.py index 596ed50343..cc69589586 100644 --- a/arkindex/ponos/tests/test_api.py +++ b/arkindex/ponos/tests/test_api.py @@ -1479,6 +1479,48 @@ class TestAPI(APITestCase): self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.json(), {"size": [expected_error]}) + @override_settings(PONOS_ARTIFACT_MAX_SIZE=99999) + def test_create_artifact_task(self): + Process.objects.create( + creator=self.user, + mode=ProcessMode.Repository, + workflow=self.workflow, + ) + + with self.assertNumQueries(5): + resp = self.client.post( + reverse("api:task-artifacts", args=[self.task1.id]), + data={ + "path": "some/path.txt", + "content_type": "text/plain", + "size": 1000, + }, + HTTP_AUTHORIZATION=f"Ponos {self.task1.token}", + ) + self.assertEqual(resp.status_code, status.HTTP_201_CREATED, resp.json()) + + data = resp.json() + s3_put_url = data.pop("s3_put_url") + self.assertTrue( + s3_put_url.startswith( + f"http://somewhere/ponos-artifacts/{self.workflow.id}/{self.task1.id}/some/path.txt" + ) + ) + + artifact = self.task1.artifacts.get() + self.assertDictEqual( + resp.json(), + { + "content_type": "text/plain", + "created": str_date(artifact.created), + "id": str(artifact.id), + "path": "some/path.txt", + "size": 1000, + "updated": str_date(artifact.updated), + "url": f"http://testserver/api/v1/task/{self.task1.id}/artifact/some/path.txt", + }, + ) + def test_artifact_download(self): # Add a new artifact -- GitLab