From 31a06a76a72b4097fce993b5131c434580084da0 Mon Sep 17 00:00:00 2001
From: Valentin Rigal <rigal@teklia.com>
Date: Wed, 24 May 2023 08:24:32 +0000
Subject: [PATCH] Restrict usage of CreateTranscriptionEntity with a worker run

---
 arkindex/documents/serializers/entities.py    |  15 +-
 arkindex/documents/tests/test_entities_api.py | 181 ++++++++++++++++--
 2 files changed, 177 insertions(+), 19 deletions(-)

diff --git a/arkindex/documents/serializers/entities.py b/arkindex/documents/serializers/entities.py
index e7d573ebfc..a77a54823d 100644
--- a/arkindex/documents/serializers/entities.py
+++ b/arkindex/documents/serializers/entities.py
@@ -349,13 +349,18 @@ class TranscriptionEntityCreateSerializer(serializers.ModelSerializer):
     offset = serializers.IntegerField(min_value=0)
     length = serializers.IntegerField(min_value=1)
     worker_version_id = ForbiddenField()
-    worker_run_id = serializers.PrimaryKeyRelatedField(
-        queryset=WorkerRun.objects.all(),
-        write_only=True,
+    worker_run_id = WorkerRunIDField(
         required=False,
+        write_only=True,
         allow_null=True,
-        style={'base_template': 'input.html'},
-        source='worker_run'
+        help_text=dedent("""
+            A WorkerRun ID that the new transcription and entity will refer to.
+
+            Regular users may only use the WorkerRuns of their own `Local` process.
+
+            Tasks authenticated via the Ponos task authentication may only use the WorkerRuns of their process.
+        """).strip(),
+
     )
     confidence = serializers.FloatField(min_value=0, max_value=1, default=None)
 
diff --git a/arkindex/documents/tests/test_entities_api.py b/arkindex/documents/tests/test_entities_api.py
index 3b9967933a..821a66f72b 100644
--- a/arkindex/documents/tests/test_entities_api.py
+++ b/arkindex/documents/tests/test_entities_api.py
@@ -440,7 +440,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_entity_worker_run_non_local(self):
         """
-        A regular user cannot create an entity with a WorkerRun of a non-local process
+        A regular user cannot create a transcription entity with a WorkerRun of a non-local process
         """
         self.client.force_login(self.superuser)
         with self.assertNumQueries(5):
@@ -783,15 +783,18 @@ class TestEntitiesAPI(FixtureAPITestCase):
         })
 
     def test_create_transcription_entity_worker_run(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        """
+        A regular user can create classifications with a WorkerRun of their own local process
+        """
+        self.client.force_login(self.superuser)
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
                 data={
                     'entity': str(self.entity.id),
                     'offset': 4,
                     'length': 8,
-                    'worker_run_id': str(self.worker_run_1.id),
+                    'worker_run_id': str(self.local_worker_run.id),
                 },
                 format='json',
             )
@@ -801,22 +804,21 @@ class TestEntitiesAPI(FixtureAPITestCase):
             'offset': 4,
             'length': 8,
             'worker_run': {
-                "id": str(self.worker_run_1.id),
-                "summary": self.worker_run_1.summary
+                "id": str(self.local_worker_run.id),
+                "summary": self.local_worker_run.summary
             },
             'confidence': None,
         })
 
-    def test_create_transcription_entity_worker_run_or_version(self):
+    def test_create_transcription_entity_forbidden_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
                 data={
                     'entity': str(self.entity.id),
                     'offset': 4,
                     'length': 8,
-                    'worker_run_id': str(self.worker_run_1.id),
                     'worker_version_id': str(self.worker_version_1.id),
                 },
                 format='json',
@@ -958,31 +960,182 @@ class TestEntitiesAPI(FixtureAPITestCase):
         })
 
     def test_create_transcription_entity_duplicate_worker_run(self):
-        self.client.force_login(self.user)
+        """
+        No duplicate entity can be created on a trancription
+        """
         TranscriptionEntity.objects.create(
             transcription=self.transcription,
             entity=self.entity,
             offset=4,
             length=8,
-            worker_run=self.worker_run_1,
+            worker_run=self.local_worker_run,
             worker_version=self.worker_version_1
         )
-        with self.assertNumQueries(8):
+
+        self.client.force_login(self.superuser)
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
                 data={
                     'entity': str(self.entity.id),
                     'offset': 4,
                     'length': 8,
-                    'worker_run_id': str(self.worker_run_1.id),
+                    'worker_run_id': str(self.local_worker_run.id),
                 },
                 format='json'
             )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
             '__all__': ['This entity is already linked to this transcription by this worker run at this position.']
         })
 
+    def test_create_transcription_entity_worker_run_non_local(self):
+        """
+        A regular user cannot create an entity on a transcription with a WorkerRun of a non-local process
+        """
+        self.client.force_login(self.superuser)
+        with self.assertNumQueries(5):
+            response = self.client.post(
+                reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
+                format='json',
+                data={
+                    'entity': str(self.entity.id),
+                    'offset': 4,
+                    'length': 8,
+                    'worker_run_id': str(self.worker_run_1.id),
+                },
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'worker_run_id': [
+                "Ponos task authentication is required to use a WorkerRun "
+                "of a process other than the user's local process."
+            ]
+        })
+
+    def test_create_transcription_entity_worker_run_other_user(self):
+        """
+        A regular user cannot create an entity on a transcription with a WorkerRun of someone else's local process
+        """
+        private_worker_run = self.user.processes.get(mode=ProcessMode.Local).worker_runs.first()
+        self.client.force_login(self.superuser)
+        with self.assertNumQueries(5):
+            response = self.client.post(
+                reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
+                format='json',
+                data={
+                    'entity': str(self.entity.id),
+                    'offset': 4,
+                    'length': 8,
+                    'worker_run_id': str(private_worker_run.id),
+                },
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'worker_run_id': [
+                "Ponos task authentication is required to use a WorkerRun "
+                "of a process other than the user's local process."
+            ]
+        })
+
+    def test_create_transcription_entity_worker_run_other_process(self):
+        """
+        A Ponos task cannot create an entity on a transcription with a WorkerRun of another process
+        """
+        process2 = self.worker_run_1.process.creator.processes.create(
+            mode=ProcessMode.Workers,
+            corpus=self.corpus,
+        )
+        other_worker_run = process2.worker_runs.create(version=self.worker_version_1, parents=[])
+        self.worker_run_1.process.start()
+        task = self.worker_run_1.process.workflow.tasks.first()
+
+        with self.assertNumQueries(6):
+            response = self.client.post(
+                reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
+                format='json',
+                data={
+                    'entity': str(self.entity.id),
+                    'offset': 4,
+                    'length': 8,
+                    'worker_run_id': str(other_worker_run.id),
+                },
+                HTTP_AUTHORIZATION=f'Ponos {task.token}',
+            )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertDictEqual(response.json(), {
+            'worker_run_id': [
+                "Only the WorkerRuns of the authenticated task's process may be used."
+            ]
+        })
+
+    def test_create_transcription_entity_worker_run_task_auth(self):
+        """
+        An entity can be created on a transcription with a WorkerRun of a non-local process
+        when authenticated as a Ponos task of this process
+        """
+        self.worker_run_1.process.start()
+        task = self.worker_run_1.process.workflow.tasks.first()
+
+        with self.assertNumQueries(8):
+            response = self.client.post(
+                reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
+                format='json',
+                data={
+                    'entity': str(self.entity.id),
+                    'offset': 4,
+                    'length': 8,
+                    'worker_run_id': str(self.worker_run_1.id),
+                },
+                HTTP_AUTHORIZATION=f'Ponos {task.token}',
+            )
+            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertDictEqual(response.json(), {
+            'entity': str(self.entity.id),
+            'offset': 4,
+            'length': 8,
+            'worker_run': {
+                "id": str(self.worker_run_1.id),
+                "summary": self.worker_run_1.summary
+            },
+            'confidence': None,
+        })
+
+    def test_create_transcription_entity_worker_run_local_task_auth(self):
+        """
+        An entity can be created on a transcription with a WorkerRun of a Local process
+        even when authenticated as a Ponos task from a different process
+        """
+        local_process = self.user.processes.get(mode=ProcessMode.Local)
+        local_worker_run = local_process.worker_runs.get()
+
+        self.worker_run_1.process.start()
+        task = self.worker_run_1.process.workflow.tasks.first()
+
+        with self.assertNumQueries(8):
+            response = self.client.post(
+                reverse('api:transcription-entity-create', kwargs={'pk': str(self.transcription.id)}),
+                format='json',
+                data={
+                    'entity': str(self.entity.id),
+                    'offset': 4,
+                    'length': 8,
+                    'worker_run_id': str(local_worker_run.id),
+                },
+                HTTP_AUTHORIZATION=f'Ponos {task.token}',
+            )
+            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertDictEqual(response.json(), {
+            'entity': str(self.entity.id),
+            'offset': 4,
+            'length': 8,
+            'worker_run': {
+                "id": str(local_worker_run.id),
+                "summary": local_worker_run.summary
+            },
+            'confidence': None,
+        })
+
     def test_create_transcription_entity_manual_existing_worker_run(self):
         """
         A manual TranscriptionEntity can be created even when one exists with the same attributes from a WorkerRun
-- 
GitLab