From 2b03ba44c963847eef534dcec9a1488b8c3a5de8 Mon Sep 17 00:00:00 2001 From: Valentin Rigal <rigal@teklia.com> Date: Wed, 24 May 2023 08:10:20 +0000 Subject: [PATCH] Restrict usage of CreateEntity with a worker run --- arkindex/documents/serializers/entities.py | 15 +- arkindex/documents/tests/test_entities_api.py | 252 +++++++++++++++--- 2 files changed, 218 insertions(+), 49 deletions(-) diff --git a/arkindex/documents/serializers/entities.py b/arkindex/documents/serializers/entities.py index cb19bed3c8..e7d573ebfc 100644 --- a/arkindex/documents/serializers/entities.py +++ b/arkindex/documents/serializers/entities.py @@ -10,7 +10,7 @@ from arkindex.documents.models import Corpus, Entity, EntityLink, EntityRole, En from arkindex.documents.serializers.light import CorpusLightSerializer, EntityTypeLightSerializer from arkindex.documents.serializers.ml import WorkerRunSummarySerializer from arkindex.process.models import WorkerRun -from arkindex.project.serializer_fields import ForbiddenField +from arkindex.project.serializer_fields import ForbiddenField, WorkerRunIDField from arkindex.project.validators import ForbiddenValidator @@ -237,11 +237,16 @@ class EntityCreateSerializer(BaseEntitySerializer): Creating new entities with a WorkerVersion is forbidden. Use `worker_run_id` instead. """), ) - worker_run_id = serializers.PrimaryKeyRelatedField( - queryset=WorkerRun.objects.all(), + worker_run_id = WorkerRunIDField( default=None, - source='worker_run', - style={'base_template': 'input.html'}, + help_text=dedent(""" + A WorkerRun ID that the new 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(), + ) type_id = serializers.PrimaryKeyRelatedField( queryset=EntityType.objects.all(), diff --git a/arkindex/documents/tests/test_entities_api.py b/arkindex/documents/tests/test_entities_api.py index 54401e0557..3b9967933a 100644 --- a/arkindex/documents/tests/test_entities_api.py +++ b/arkindex/documents/tests/test_entities_api.py @@ -26,6 +26,7 @@ class TestEntitiesAPI(FixtureAPITestCase): cls.private_corpus = Corpus.objects.create(name='private') cls.worker_version_1 = WorkerVersion.objects.get(worker__slug='reco') cls.worker_version_2 = WorkerVersion.objects.get(worker__slug='dla') + cls.local_worker_run = cls.worker_version_1.worker_runs.filter(process__mode=ProcessMode.Local).get() cls.worker_run_1 = cls.worker_version_1.worker_runs.filter(process__mode=ProcessMode.Workers).get() cls.worker_run_2 = cls.worker_version_2.worker_runs.filter(process__mode=ProcessMode.Workers).get() cls.page = cls.corpus.elements.get(name='Volume 1, page 1r') @@ -370,7 +371,10 @@ class TestEntitiesAPI(FixtureAPITestCase): response = self.client.post(reverse('api:entity-create'), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_create_entity_with_worker_version(self): + def test_create_entity_no_worker_version(self): + """ + Only a worker run can be used to create an entity, not a worker version + """ data = { 'name': 'entity', 'type_id': str(self.person_type.id), @@ -389,19 +393,149 @@ class TestEntitiesAPI(FixtureAPITestCase): 'worker_version': ['This field is forbidden.'], }) - def test_create_entity_with_worker_run(self): + def test_create_entity_with_unknown_worker_run(self): data = { 'name': 'entity', 'type_id': str(self.person_type.id), 'corpus': str(self.corpus.id), - 'worker_run_id': str(self.worker_run_1.id), + 'worker_run_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'metas': { 'key': 'value', 'other key': 'other value' }, } self.client.force_login(self.user) - with self.assertNumQueries(10): + with self.assertNumQueries(7): + response = self.client.post(reverse('api:entity-create'), data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertDictEqual(response.json(), {'worker_run_id': ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']}) + + def test_create_entity(self): + """ + A user can create an entity without a worker run + """ + self.client.force_login(self.user) + with self.assertNumQueries(9): + response = self.client.post( + reverse('api:entity-create'), + format='json', + data={ + 'name': 'test', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + 'metas': {'pokemon': 'Wo-Chien'}, + }, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = response.json() + entity = Entity.objects.get(id=data['id']) + self.assertEqual(entity.name, 'test') + self.assertEqual(entity.type, self.person_type) + self.assertEqual(entity.corpus, self.corpus) + self.assertFalse(entity.validated) + self.assertEqual(entity.metas, {'pokemon': 'Wo-Chien'}) + self.assertEqual(entity.worker_version_id, None) + self.assertEqual(entity.worker_run, None) + + def test_create_entity_worker_run_non_local(self): + """ + A regular user cannot create an entity with a WorkerRun of a non-local process + """ + self.client.force_login(self.superuser) + with self.assertNumQueries(5): + response = self.client.post( + reverse('api:entity-create'), + format='json', + data={ + 'name': 'test', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + '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_entity_worker_run_other_user(self): + """ + A regular user cannot create an entity 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:entity-create'), + format='json', + data={ + 'name': 'test', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + '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_entity_worker_run_other_process(self): + """ + A Ponos task cannot create an entity 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:entity-create'), + format='json', + data={ + 'name': 'Bunnelby', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + '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_entity_local_worker_run(self): + """ + A regular user can create an entity with a WorkerRun of their own local process + """ + data = { + 'name': 'entity', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + 'worker_run_id': str(self.local_worker_run.id), + 'metas': { + 'key': 'value', + 'other key': 'other value' + }, + } + self.client.force_login(self.superuser) + with self.assertNumQueries(8): response = self.client.post(reverse('api:entity-create'), data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -415,8 +549,8 @@ class TestEntitiesAPI(FixtureAPITestCase): 'key': 'value', 'other key': 'other value', }) - self.assertEqual(entity.worker_version, self.worker_run_1.version) - self.assertEqual(entity.worker_run, self.worker_run_1) + self.assertEqual(entity.worker_version_id, self.local_worker_run.version_id) + self.assertEqual(entity.worker_run, self.local_worker_run) self.assertDictEqual(data, { 'id': str(entity.id), @@ -437,50 +571,80 @@ class TestEntitiesAPI(FixtureAPITestCase): }, 'validated': False, '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, }, - 'worker_version_id': str(self.worker_version_1.id), + 'worker_version_id': str(self.local_worker_run.version_id), 'parents': [], 'children': [], }) - def test_create_entity_with_unknown_worker_run(self): - data = { - 'name': 'entity', - 'type_id': str(self.person_type.id), - 'corpus': str(self.corpus.id), - 'worker_run_id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'metas': { - 'key': 'value', - 'other key': 'other value' - }, - } - self.client.force_login(self.user) - with self.assertNumQueries(7): - response = self.client.post(reverse('api:entity-create'), data=data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertDictEqual(response.json(), {'worker_run_id': ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']}) + def test_create_entity_task_auth(self): + """ + An entity can be created 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() - def test_create_entity_with_worker_run_and_worker_version(self): - data = { - 'name': 'entity', - 'type_id': str(self.person_type.id), - 'corpus': str(self.corpus.id), - 'worker_version': str(self.worker_version_1.id), - 'worker_run_id': str(self.worker_run_1.id), - 'metas': { - 'key': 'value', - 'other key': 'other value' - }, - } - self.client.force_login(self.user) - with self.assertNumQueries(7): - response = self.client.post(reverse('api:entity-create'), data=data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertDictEqual(response.json(), { - 'worker_version': ['This field is forbidden.'], - }) + with self.assertNumQueries(9): + response = self.client.post( + reverse('api:entity-create'), + format='json', + data={ + 'name': 'test', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + 'worker_run_id': str(self.worker_run_1.id), + }, + HTTP_AUTHORIZATION=f'Ponos {task.token}', + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = response.json() + entity = Entity.objects.get(id=data['id']) + self.assertEqual(entity.name, 'test') + self.assertEqual(entity.type, self.person_type) + self.assertEqual(entity.corpus, self.corpus) + self.assertFalse(entity.validated) + self.assertEqual(entity.metas, None) + self.assertEqual(entity.worker_version, self.worker_version_1) + self.assertEqual(entity.worker_run, self.worker_run_1) + + def test_create_entity_worker_run_local_task_auth(self): + """ + An entity can be created 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(9): + response = self.client.post( + reverse('api:entity-create'), + format='json', + data={ + 'name': 'Bunnelby', + 'type_id': str(self.person_type.id), + 'corpus': str(self.corpus.id), + 'worker_run_id': str(local_worker_run.id), + }, + HTTP_AUTHORIZATION=f'Ponos {task.token}', + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + data = response.json() + entity = Entity.objects.get(id=data['id']) + self.assertEqual(entity.name, 'Bunnelby') + self.assertEqual(entity.type, self.person_type) + self.assertEqual(entity.corpus, self.corpus) + self.assertFalse(entity.validated) + self.assertEqual(entity.metas, None) + self.assertEqual(entity.worker_version_id, local_worker_run.version_id) + self.assertEqual(entity.worker_run, local_worker_run) def test_create_link(self): child = Entity.objects.create( -- GitLab