diff --git a/src/openapi.yaml b/src/openapi.yaml index bb360371c0..3d1e0bd245 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -3801,6 +3801,71 @@ paths: $ref: '#/components/headers/X-Is-Form-Designer' Content-Language: $ref: '#/components/headers/Content-Language' + /api/v2/registration/plugins/objects-api/object-types: + get: + operationId: registration_plugins_objects_api_object_types_list + description: |- + List the available Objecttypes. + + Note that the response data is essentially proxied from the configured Objecttypes API. + tags: + - registration + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Objecttype' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' + /api/v2/registration/plugins/objects-api/object-types/{submission_uuid}/versions: + get: + operationId: registration_plugins_objects_api_object_types_versions_list + description: |- + List the available versions for an Objecttype. + + Note that the response data is essentially proxied from the configured Objecttypes API. + parameters: + - in: path + name: submission_uuid + schema: + type: string + format: uuid + required: true + tags: + - registration + security: + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ObjecttypeVersion' + description: '' + headers: + X-Session-Expires-In: + $ref: '#/components/headers/X-Session-Expires-In' + X-CSRFToken: + $ref: '#/components/headers/X-CSRFToken' + X-Is-Form-Designer: + $ref: '#/components/headers/X-Is-Form-Designer' + Content-Language: + $ref: '#/components/headers/Content-Language' /api/v2/service-fetch-configurations: get: operationId: service_fetch_configurations_list @@ -8410,6 +8475,45 @@ components: - isApplicable - name - url + Objecttype: + type: object + properties: + url: + type: string + format: uri + title: URL reference to this object. This is the unique identification and + location of this object. + uuid: + type: string + format: uuid + title: Unique identifier (UUID4). + name: + type: string + title: Name of the object type. + namePlural: + type: string + title: Plural name of the object type. + dataClassification: + type: string + title: Confidential level of the object type. + required: + - dataClassification + - name + - namePlural + - url + - uuid + ObjecttypeVersion: + type: object + properties: + version: + type: integer + title: Integer version of the Objecttype. + status: + type: string + title: Status of the object type version + required: + - status + - version PaginatedFormDefinitionList: type: object properties: diff --git a/src/openforms/registrations/api/urls.py b/src/openforms/registrations/api/urls.py index 51f78eac3e..25753a1618 100644 --- a/src/openforms/registrations/api/urls.py +++ b/src/openforms/registrations/api/urls.py @@ -21,4 +21,8 @@ # TODO: make this dynamic and include it through the registry? urlpatterns += [ path("plugins/camunda/", include("openforms.registrations.contrib.camunda.api")), + path( + "plugins/objects-api/", + include("openforms.registrations.contrib.objects_api.api.urls"), + ), ] diff --git a/src/openforms/registrations/contrib/objects_api/api/__init__.py b/src/openforms/registrations/contrib/objects_api/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/registrations/contrib/objects_api/api/serializers.py b/src/openforms/registrations/contrib/objects_api/api/serializers.py new file mode 100644 index 0000000000..b9fa46883f --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/api/serializers.py @@ -0,0 +1,25 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework import serializers + + +class ObjecttypeSerializer(serializers.Serializer): + # Keys are defined in camel case as this is what we get from the Objecttype API + url = serializers.URLField( + label=_( + "URL reference to this object. This is the unique identification and location of this object." + ), + ) + uuid = serializers.UUIDField(label=_("Unique identifier (UUID4).")) + name = serializers.CharField(label=_("Name of the object type.")) + namePlural = serializers.CharField(label=_("Plural name of the object type.")) + dataClassification = serializers.CharField( + label=_("Confidential level of the object type.") + ) + + +class ObjecttypeVersionSerializer(serializers.Serializer): + version = serializers.IntegerField( + label=_("Integer version of the Objecttype."), + ) + status = serializers.CharField(label=_("Status of the object type version")) diff --git a/src/openforms/registrations/contrib/objects_api/api/urls.py b/src/openforms/registrations/contrib/objects_api/api/urls.py new file mode 100644 index 0000000000..63dc2bd71d --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/api/urls.py @@ -0,0 +1,18 @@ +from django.urls import path + +from .views import ObjecttypesListView, ObjecttypeVersionsListView + +app_name = "objects_api" + +urlpatterns = [ + path( + "object-types", + ObjecttypesListView.as_view(), + name="object-types", + ), + path( + "object-types//versions", + ObjecttypeVersionsListView.as_view(), + name="object-type-versions", + ), +] diff --git a/src/openforms/registrations/contrib/objects_api/api/views.py b/src/openforms/registrations/contrib/objects_api/api/views.py new file mode 100644 index 0000000000..5a111b8c37 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/api/views.py @@ -0,0 +1,51 @@ +from typing import Any + +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import authentication, permissions, views + +from openforms.api.views import ListMixin + +from ..client import get_objecttypes_client +from .serializers import ObjecttypeSerializer, ObjecttypeVersionSerializer + + +@extend_schema_view( + get=extend_schema( + tags=["registration"], + ), +) +class ObjecttypesListView(ListMixin, views.APIView): + """ + List the available Objecttypes. + + Note that the response data is essentially proxied from the configured Objecttypes API. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = ObjecttypeSerializer + + def get_objects(self) -> list[dict[str, Any]]: + with get_objecttypes_client() as client: + return client.list_objecttypes() + + +@extend_schema_view( + get=extend_schema( + tags=["registration"], + ), +) +class ObjecttypeVersionsListView(ListMixin, views.APIView): + """ + List the available versions for an Objecttype. + + Note that the response data is essentially proxied from the configured Objecttypes API. + """ + + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (permissions.IsAdminUser,) + serializer_class = ObjecttypeVersionSerializer + + def get_objects(self) -> list[dict[str, Any]]: + with get_objecttypes_client() as client: + return client.list_objecttype_versions(self.kwargs["submission_uuid"]) diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_verions_unknown_objecttype.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_verions_unknown_objecttype.yaml new file mode 100644 index 0000000000..bbc4f89c37 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_verions_unknown_objecttype.yaml @@ -0,0 +1,36 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8001/api/v2/objecttypes/39da819c-ac6c-4037-ae2b-6bfc39f6564b/versions + response: + body: + string: '{"count":0,"next":null,"previous":null,"results":[]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Length: + - '52' + Content-Type: + - application/json + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_versions.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_versions.yaml new file mode 100644 index 0000000000..f89fab844b --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_list_objecttype_versions.yaml @@ -0,0 +1,37 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions + response: + body: + string: '{"count":1,"next":null,"previous":null,"results":[{"url":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1","version":1,"objectType":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","status":"published","jsonSchema":{"$id":"https://example.com/tree.schema.json","type":"object","title":"Tree","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"height":{"type":"integer","description":"The + height of the tree."}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"}]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Length: + - '585' + Content-Type: + - application/json + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_staff_user_required.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_staff_user_required.yaml new file mode 100644 index 0000000000..f89fab844b --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypeVersionsAPIEndpointsTests/ObjecttypeVersionsAPIEndpointsTests.test_staff_user_required.yaml @@ -0,0 +1,37 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions + response: + body: + string: '{"count":1,"next":null,"previous":null,"results":[{"url":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1","version":1,"objectType":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","status":"published","jsonSchema":{"$id":"https://example.com/tree.schema.json","type":"object","title":"Tree","$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"height":{"type":"integer","description":"The + height of the tree."}}},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","publishedAt":"2024-02-08"}]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Length: + - '585' + Content-Type: + - application/json + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_list_objecttypes.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_list_objecttypes.yaml new file mode 100644 index 0000000000..cdabace3c7 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_list_objecttypes.yaml @@ -0,0 +1,36 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8001/api/v2/objecttypes + response: + body: + string: '{"count":2,"next":null,"previous":null,"results":[{"url":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","uuid":"3edfdaf7-f469-470b-a391-bb7ea015bd6f","name":"Tree","namePlural":"Trees","description":"","dataClassification":"confidential","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1"]},{"url":"http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","uuid":"8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","name":"Person","namePlural":"Persons","description":"","dataClassification":"open","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2023-10-24","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/1","http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/2","http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/3"]}]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Length: + - '1407' + Content-Type: + - application/json + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_staff_user_required.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_staff_user_required.yaml new file mode 100644 index 0000000000..cdabace3c7 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/ObjecttypesAPIEndpointsTests/ObjecttypesAPIEndpointsTests.test_staff_user_required.yaml @@ -0,0 +1,36 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 171be5abaf41e7856b423ad513df1ef8f867ff48 + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: http://localhost:8001/api/v2/objecttypes + response: + body: + string: '{"count":2,"next":null,"previous":null,"results":[{"url":"http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f","uuid":"3edfdaf7-f469-470b-a391-bb7ea015bd6f","name":"Tree","namePlural":"Trees","description":"","dataClassification":"confidential","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2024-02-08","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f/versions/1"]},{"url":"http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","uuid":"8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","name":"Person","namePlural":"Persons","description":"","dataClassification":"open","maintainerOrganization":"","maintainerDepartment":"","contactPerson":"","contactEmail":"","source":"","updateFrequency":"unknown","providerOrganization":"","documentationUrl":"","labels":{},"createdAt":"2023-10-24","modifiedAt":"2024-02-08","allowGeometry":true,"versions":["http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/1","http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/2","http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48/versions/3"]}]}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Content-Length: + - '1407' + Content-Type: + - application/json + Referrer-Policy: + - same-origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py b/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py new file mode 100644 index 0000000000..5f47403d50 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/test_api_endpoints.py @@ -0,0 +1,146 @@ +from pathlib import Path +from unittest.mock import patch + +from rest_framework import status +from rest_framework.reverse import reverse_lazy +from rest_framework.test import APITestCase + +from openforms.accounts.tests.factories import StaffUserFactory, UserFactory +from openforms.utils.tests.vcr import OFVCRMixin + +from .test_objecttypes_client import get_test_config + + +class ObjecttypesAPIEndpointsTests(OFVCRMixin, APITestCase): + + VCR_TEST_FILES = Path(__file__).parent / "files" + + def setUp(self) -> None: + super().setUp() + + self.endpoint = reverse_lazy("api:objects_api:object-types") + + patcher = patch( + "openforms.registrations.contrib.objects_api.client.ObjectsAPIConfig.get_solo", + return_value=get_test_config(), + ) + + self.config_mock = patcher.start() + self.addCleanup(patcher.stop) + + def test_auth_required(self): + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_staff_user_required(self): + user = UserFactory.create() + staff_user = StaffUserFactory.create() + + with self.subTest(staff=False): + self.client.force_authenticate(user=user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + with self.subTest(staff=True): + self.client.force_authenticate(user=staff_user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_list_objecttypes(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json(), + [ + { + "dataClassification": "confidential", + "name": "Tree", + "namePlural": "Trees", + "url": "http://localhost:8001/api/v2/objecttypes/3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "uuid": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + }, + { + "dataClassification": "open", + "name": "Person", + "namePlural": "Persons", + "url": "http://localhost:8001/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + }, + ], + ) + + +class ObjecttypeVersionsAPIEndpointsTests(OFVCRMixin, APITestCase): + + VCR_TEST_FILES = Path(__file__).parent / "files" + + def setUp(self) -> None: + super().setUp() + self.objecttype_uuid = "3edfdaf7-f469-470b-a391-bb7ea015bd6f" + self.endpoint = reverse_lazy( + "api:objects_api:object-type-versions", args=[self.objecttype_uuid] + ) + + patcher = patch( + "openforms.registrations.contrib.objects_api.client.ObjectsAPIConfig.get_solo", + return_value=get_test_config(), + ) + + self.config_mock = patcher.start() + self.addCleanup(patcher.stop) + + def test_auth_required(self): + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_staff_user_required(self): + user = UserFactory.create() + staff_user = StaffUserFactory.create() + + with self.subTest(staff=False): + self.client.force_authenticate(user=user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + with self.subTest(staff=True): + self.client.force_authenticate(user=staff_user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_list_objecttype_versions(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + response = self.client.get(self.endpoint) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), [{"status": "published", "version": 1}]) + + def test_list_objecttype_verions_unknown_objecttype(self): + staff_user = StaffUserFactory.create() + self.client.force_authenticate(user=staff_user) + + # This UUID doesn't exist: + response = self.client.get( + reverse_lazy( + "api:objects_api:object-type-versions", + args=["39da819c-ac6c-4037-ae2b-6bfc39f6564b"], + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), [])