From 35d6a172a320a8e949821850626313d22171be8e Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 10 Oct 2024 12:22:21 +0200 Subject: [PATCH 01/39] :sparkles: [#4398] Pass initial_data_reference from auth start to return view the initial data reference is passed from the SDK to the backend as part of the login URL. In order to pass it as part of the submission create body, it must be propagated from the authentication start/return views back to the SDK --- src/openforms/authentication/views.py | 14 ++++++++++++++ src/openforms/frontend/frontend.py | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/openforms/authentication/views.py b/src/openforms/authentication/views.py index bd298e8acb..060033108b 100644 --- a/src/openforms/authentication/views.py +++ b/src/openforms/authentication/views.py @@ -166,6 +166,11 @@ class AuthenticationStartView(AuthenticationFlowBaseView): register = register def get(self, request: Request, slug: str, plugin_id: str): + # Store the initial data reference in the session, so it can be passed back + # in the `AuthenticationReturnView` + request.session["initial_data_reference"] = request.query_params.get( + "initial_data_reference" + ) form = self.get_object() try: plugin = self.register[plugin_id] @@ -300,6 +305,7 @@ def _handle_return(self, request: Request, slug: str, plugin_id: str): plugin. We must define ``get`` and ``post`` to have them properly show up and be documented in the OAS. """ + initial_data_reference = request.session.pop("initial_data_reference", None) form = self.get_object() try: plugin = self.register[plugin_id] @@ -339,6 +345,14 @@ def _handle_return(self, request: Request, slug: str, plugin_id: str): if hasattr(request, "session") and FORM_AUTH_SESSION_KEY in request.session: authentication_success.send(sender=self.__class__, request=request) + if initial_data_reference: + # Pass the initial data reference back to the SDK, to allow sending it + # with the submission create call + return HttpResponseRedirect( + furl(response.url) + .add({"initial_data_reference": initial_data_reference}) + .url + ) return response def _handle_co_sign(self, form: Form, plugin: BasePlugin) -> None: diff --git a/src/openforms/frontend/frontend.py b/src/openforms/frontend/frontend.py index 896bf8ddc2..010b29da72 100644 --- a/src/openforms/frontend/frontend.py +++ b/src/openforms/frontend/frontend.py @@ -26,6 +26,9 @@ def get_frontend_redirect_url( f = submission.cleaned_form_url f.query.remove("_of_action") f.query.remove("_of_action_params") + # Remove any data references, since this should already be stored on the + # Submission + f.query.remove("initial_data_reference") _query = {"_of_action": action} if action else {} if action_params: _query["_of_action_params"] = json.dumps(action_params) From 0aa3b9dd90a350caa824a547018e5fa732566abf Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 22 Oct 2024 16:20:19 +0200 Subject: [PATCH 02/39] :sparkles: [#4398] Check object ownership during prefill and pre-registration if initial_data_reference is specified, Prefill plugins will verify ownership before attempting to prefill and registration plugins will do the same during pre registration --- .../contrib/objects_api/validators.py | 59 ++++++++++++++++++- src/openforms/prefill/base.py | 11 ++++ .../prefill/contrib/objects_api/plugin.py | 5 ++ src/openforms/prefill/sources.py | 5 ++ src/openforms/registrations/base.py | 11 ++++ .../contrib/objects_api/plugin.py | 4 ++ src/openforms/registrations/tasks.py | 14 +++++ 7 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/openforms/contrib/objects_api/validators.py b/src/openforms/contrib/objects_api/validators.py index cde3d2a987..5c98f57684 100644 --- a/src/openforms/contrib/objects_api/validators.py +++ b/src/openforms/contrib/objects_api/validators.py @@ -1,11 +1,20 @@ from __future__ import annotations +import logging import warnings from functools import partial +from typing import TYPE_CHECKING -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError from django.utils.translation import gettext_lazy as _ +from glom import glom +from requests.exceptions import RequestException + +from openforms.contrib.objects_api.clients import ( + NoServiceConfigured, + get_objects_client, +) from openforms.contrib.zgw.clients.catalogi import Catalogus from openforms.contrib.zgw.validators import ( validate_catalogue_reference as _validate_catalogue_reference, @@ -14,6 +23,11 @@ from .clients import get_catalogi_client from .models import ObjectsAPIGroupConfig +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from openforms.submissions.models import Submission + def validate_catalogue_reference( config: ObjectsAPIGroupConfig, @@ -78,3 +92,46 @@ def validate_document_type_references( if errors: raise ValidationError(errors) + + +def validate_object_ownership(submission: Submission) -> None: + form = submission.form + + try: + auth_info = submission.auth_info + except ObjectDoesNotExist: + raise PermissionDenied("Cannot pass data reference as anonymous user") + + object = None + for backend in form.registration_backends.filter(backend="objects_api"): + if not backend.options: + continue + + api_group = ObjectsAPIGroupConfig.objects.filter( + pk=backend.options.get("objects_api_group") + ).first() + if not api_group: + continue + + try: + with get_objects_client(api_group) as client: + try: + object = client.get_object(submission.initial_data_reference) + break + except RequestException: + logger.exception( + "Something went wrong while trying to retrieve " + "object for initial_data_reference" + ) + except NoServiceConfigured: + logger.exception( + "Something went wrong while trying to create a client " + "for Objects API" + ) + + if not object: + raise PermissionDenied("Could not fetch object for initial data reference") + + # TODO should this path be configurable? + if glom(object["record"]["data"], auth_info.attribute) != auth_info.value: + raise PermissionDenied("User is not the owner of the referenced object") diff --git a/src/openforms/prefill/base.py b/src/openforms/prefill/base.py index 584513cb59..eaa3ba8aa2 100644 --- a/src/openforms/prefill/base.py +++ b/src/openforms/prefill/base.py @@ -129,3 +129,14 @@ def get_identifier_value( @classmethod def configuration_context(cls) -> JSONObject | None: return None + + def verify_initial_data_ownership(self, submission: Submission) -> None: + """ + Hook to check if the authenticated user is the owner of the object + referenced to by `initial_data_reference + + :param submission: an active :class:`Submission` instance + """ + raise NotImplementedError( + "You must implement the 'verify_initial_data_ownership' method." + ) diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index 0756cbafb6..019a2dfe8b 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -8,6 +8,7 @@ from openforms.contrib.objects_api.checks import check_config from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig +from openforms.contrib.objects_api.validators import validate_object_ownership from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig from openforms.submissions.models import Submission from openforms.typing import JSONEncodable, JSONObject @@ -19,6 +20,7 @@ logger = logging.getLogger(__name__) + PLUGIN_IDENTIFIER = "objects_api" @@ -79,3 +81,6 @@ def configuration_context(cls) -> JSONObject | None: for group in ObjectsAPIGroupConfig.objects.iterator() ] } + + def verify_initial_data_ownership(self, submission: Submission) -> None: + validate_object_ownership(submission) diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index 8ae0b56139..ef68e0be0f 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -54,6 +54,11 @@ def invoke_plugin( if not plugin.is_enabled: raise PluginNotEnabled() + # If an `initial_data_reference` was passed, we must verify that the + # authenticated user is the owner of the referenced object + if submission.initial_data_reference: + plugin.verify_initial_data_ownership(submission) + attributes = [attribute for field in fields for attribute in field] try: values = plugin.get_prefill_values(submission, attributes, identifier_role) diff --git a/src/openforms/registrations/base.py b/src/openforms/registrations/base.py index e30eb5b327..c68f3d813d 100644 --- a/src/openforms/registrations/base.py +++ b/src/openforms/registrations/base.py @@ -84,3 +84,14 @@ def get_variables(self) -> list[FormVariable]: Return the static variables for this registration plugin. """ return [] + + def verify_initial_data_ownership(self, submission: Submission) -> None: + """ + Hook to check if the authenticated user is the owner of the object + referenced to by `initial_data_reference + + :param submission: an active :class:`Submission` instance + """ + raise NotImplementedError( + "You must implement the 'verify_initial_data_ownership' method." + ) diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 2767da37b9..e4437b7263 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -13,6 +13,7 @@ get_objects_client, get_objecttypes_client, ) +from openforms.contrib.objects_api.validators import validate_object_ownership from openforms.registrations.utils import execute_unless_result_exists from openforms.variables.service import get_static_variables @@ -170,3 +171,6 @@ def update_payment_status( @override def get_variables(self) -> list[FormVariable]: return get_static_variables(variables_registry=variables_registry) + + def verify_initial_data_ownership(self, submission: Submission) -> None: + validate_object_ownership(submission) diff --git a/src/openforms/registrations/tasks.py b/src/openforms/registrations/tasks.py index a8f5497819..1ac00809d1 100644 --- a/src/openforms/registrations/tasks.py +++ b/src/openforms/registrations/tasks.py @@ -2,6 +2,7 @@ import traceback from contextlib import contextmanager +from django.core.exceptions import PermissionDenied from django.db import transaction from django.utils import timezone @@ -81,6 +82,19 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: submission.save() return + # If an `initial_data_reference` was passed, we must verify that the + # authenticated user is the owner of the referenced object + if submission.initial_data_reference: + try: + registration_plugin.verify_initial_data_ownership(submission) + except PermissionDenied as e: + logger.exception( + "Submission with initial_data_reference did not pass ownership check for plugin %s", + registration_plugin.verbose_name, + ) + logevent.registration_failure(submission, e, plugin=registration_plugin) + raise e + options_serializer = registration_plugin.configuration_options( data=submission.registration_backend.options, context={"validate_business_logic": False}, From ebbf112a298709548bcba741ce011a2c418ba595 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 22 Oct 2024 16:21:07 +0200 Subject: [PATCH 03/39] :white_check_mark: [#4398] Add tests for initial_data_reference ownership check --- ...ests.test_user_is_not_owner_of_object.yaml | 50 ++++ ...torTests.test_user_is_owner_of_object.yaml | 50 ++++ .../objects_api/tests/test_validators.py | 216 ++++++++++++++++++ .../prefill/tests/test_prefill_variables.py | 100 ++++++++ .../tests/test_pre_registration.py | 54 ++++- 5 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml create mode 100644 src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml create mode 100644 src/openforms/contrib/objects_api/tests/test_validators.py diff --git a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml new file mode 100644 index 0000000000..1b4c6115d9 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94","uuid":"7cdf3889-edd2-4b65-8140-a36e96dfbb94","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-28","endAt":null,"registrationAt":"2024-10-28","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '422' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 28 Oct 2024 08:35:39 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml new file mode 100644 index 0000000000..1b4c6115d9 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94","uuid":"7cdf3889-edd2-4b65-8140-a36e96dfbb94","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-28","endAt":null,"registrationAt":"2024-10-28","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '422' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 28 Oct 2024 08:35:39 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/test_validators.py b/src/openforms/contrib/objects_api/tests/test_validators.py new file mode 100644 index 0000000000..12e22b4ac8 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/test_validators.py @@ -0,0 +1,216 @@ +from pathlib import Path +from unittest.mock import patch + +from django.core.exceptions import PermissionDenied +from django.test import TestCase, override_settings, tag + +from requests.exceptions import RequestException + +from openforms.authentication.service import AuthAttribute +from openforms.contrib.objects_api.clients import get_objects_client +from openforms.contrib.objects_api.helpers import prepare_data_for_registration +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.forms.tests.factories import FormRegistrationBackendFactory +from openforms.submissions.tests.factories import SubmissionFactory +from openforms.utils.tests.vcr import OFVCRMixin + +from ..validators import validate_object_ownership + +TEST_FILES = (Path(__file__).parent / "data").resolve() + + +@override_settings( + CORS_ALLOW_ALL_ORIGINS=False, + ALLOWED_HOSTS=["*"], + CORS_ALLOWED_ORIGINS=["http://testserver.com"], +) +class ObjectsAPIInitialDataOwnershipValidatorTests(OFVCRMixin, TestCase): + VCR_TEST_FILES = TEST_FILES + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + # TODO fix tests failing after first run + with get_objects_client(cls.objects_api_group_used) as client: + object = client.create_object( + record_data=prepare_data_for_registration( + data={"bsn": "111222333", "foo": "bar"}, + objecttype_version=1, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + ) + cls.object_ref = object["uuid"] + + @tag("gh-4398") + def test_user_is_owner_of_object(self): + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + + # An objects API backend with a different API group + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": 5, + "objecttype_version": 1, + }, + ) + # Another backend that should be ignored + FormRegistrationBackendFactory.create(form=submission.form, backend="email") + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + validate_object_ownership(submission) + + @tag("gh-4398") + def test_permission_denied_if_user_is_not_logged_in(self): + submission = SubmissionFactory.create(initial_data_reference=self.object_ref) + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "Cannot pass data reference as anonymous user" + ) + + @tag("gh-4398") + def test_user_is_not_owner_of_object(self): + submission = SubmissionFactory.create( + auth_info__value="123456782", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "User is not the owner of the referenced object" + ) + + @tag("gh-4398") + @patch( + "openforms.contrib.objects_api.clients.objects.ObjectsClient.get_object", + side_effect=RequestException, + ) + def test_request_exception_when_doing_permission_check(self, mock_get_object): + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "Could not fetch object for initial data reference" + ) + + @tag("gh-4398") + def test_no_objects_service_configured_raises_error( + self, + ): + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + + objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + objects_service=None, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "Could not fetch object for initial data reference" + ) + + @tag("gh-4398") + def test_no_backends_configured_raises_error( + self, + ): + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + FormRegistrationBackendFactory.create(form=submission.form, backend="email") + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "Could not fetch object for initial data reference" + ) + + @tag("gh-4398") + def test_backend_without_options_raises_error( + self, + ): + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + ObjectsAPIGroupConfigFactory.create(for_test_docker_compose=True) + FormRegistrationBackendFactory.create( + form=submission.form, backend="objects_api", options={} + ) + + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission) + self.assertEqual( + str(cm.exception), "Could not fetch object for initial data reference" + ) diff --git a/src/openforms/prefill/tests/test_prefill_variables.py b/src/openforms/prefill/tests/test_prefill_variables.py index 07e47ecf33..4944c14b7a 100644 --- a/src/openforms/prefill/tests/test_prefill_variables.py +++ b/src/openforms/prefill/tests/test_prefill_variables.py @@ -1,5 +1,6 @@ from unittest.mock import patch +from django.core.exceptions import PermissionDenied from django.test import RequestFactory, TestCase, TransactionTestCase import requests_mock @@ -18,6 +19,7 @@ FormVariableFactory, ) from openforms.logging.models import TimelineLogProxy +from openforms.prefill.contrib.demo.constants import Attributes from openforms.submissions.constants import SubmissionValueVariableSources from openforms.submissions.tests.factories import ( SubmissionFactory, @@ -318,3 +320,101 @@ def test_no_success_message_on_failure(self, m, m_solo): for log in logs: self.assertNotEqual(log.event, "prefill_retrieve_success") + + def test_verify_initial_data_ownership(self): + form_step = FormStepFactory.create( + form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + "prefill": { + "plugin": "demo", + "attribute": Attributes.random_string, + }, + "defaultValue": "", + } + ] + } + ) + + with self.subTest( + "verify_initial_data_ownership is not called if initial_data_reference is not specified" + ): + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + ) + + with patch( + "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" + ) as mock_verify_ownership: + prefill_variables(submission=submission_step.submission) + + mock_verify_ownership.assert_not_called() + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 + ) + + with self.subTest( + "verify_initial_data_ownership is called if initial_data_reference is specified" + ): + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference="1234", + ) + + with patch( + "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" + ) as mock_verify_ownership: + prefill_variables(submission=submission_step.submission) + + mock_verify_ownership.assert_called_once_with( + submission_step.submission + ) + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 + ) + + with self.subTest( + "verify_initial_data_ownership raising error causes prefill to fail" + ): + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference="1234", + ) + + with patch( + "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership", + side_effect=PermissionDenied, + ) as mock_verify_ownership: + with self.assertRaises(PermissionDenied): + prefill_variables(submission=submission_step.submission) + + mock_verify_ownership.assert_called_once_with( + submission_step.submission + ) + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) diff --git a/src/openforms/registrations/tests/test_pre_registration.py b/src/openforms/registrations/tests/test_pre_registration.py index 7b422c8bbc..66ba6c6b31 100644 --- a/src/openforms/registrations/tests/test_pre_registration.py +++ b/src/openforms/registrations/tests/test_pre_registration.py @@ -1,6 +1,7 @@ from unittest.mock import patch -from django.test import TestCase +from django.core.exceptions import PermissionDenied +from django.test import TestCase, tag from rest_framework.exceptions import ValidationError from testfixtures import LogCapture @@ -374,3 +375,54 @@ def test_traceback_removed_from_result_after_success(self, m_get_solo): submission.refresh_from_db() self.assertNotIn("traceback", submission.registration_result) + + @tag("gh-4398") + def test_verify_initial_data_ownership(self): + with self.subTest( + "verify_initial_data_ownership is not called if no initial_data_reference is specified" + ): + submission = SubmissionFactory.create( + form__registration_backend="demo", + completed_not_preregistered=True, + ) + + with patch( + "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership" + ) as mock_verify_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + mock_verify_ownership.assert_not_called() + + with self.subTest( + "verify_initial_data_ownership is called if initial_data_reference exists is specified" + ): + submission = SubmissionFactory.create( + form__registration_backend="demo", + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership" + ) as mock_verify_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + mock_verify_ownership.assert_called_once_with(submission) + + with self.subTest( + "verify_initial_data_ownership raising error causes pre registration to fail" + ): + submission = SubmissionFactory.create( + form__registration_backend="demo", + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership", + side_effect=PermissionDenied, + ) as mock_verify_ownership: + with self.assertRaises(PermissionDenied): + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + mock_verify_ownership.assert_called_once_with(submission) From 20462307778d477a6d41f085039c30df1c61af93 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 29 Oct 2024 13:04:54 +0100 Subject: [PATCH 04/39] :recycle: [#4398] Modify ownership check behavior in case of errors if the object could not be fetched for whatever reason, no longer raise a PermissionDenied, because we could not explicitly verify that the user is not the owner of the object. This is needed because in case of multiple prefill/registration backends, only one of them will actually contain the object, so we do not want to break the form due to 404 errors in the others --- ...ests.test_user_is_not_owner_of_object.yaml | 6 +-- ...torTests.test_user_is_owner_of_object.yaml | 6 +-- .../objects_api/tests/test_validators.py | 42 +++++++++---------- .../contrib/objects_api/validators.py | 8 +++- 4 files changed, 34 insertions(+), 28 deletions(-) rename src/openforms/contrib/objects_api/tests/{data => files}/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml (70%) rename src/openforms/contrib/objects_api/tests/{data => files}/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml (70%) diff --git a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml similarity index 70% rename from src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml index 1b4c6115d9..2b4cc25e02 100644 --- a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94 + uri: http://localhost:8002/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94","uuid":"7cdf3889-edd2-4b65-8140-a36e96dfbb94","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-28","endAt":null,"registrationAt":"2024-10-28","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694","uuid":"5015053e-1916-42b5-9119-ede73f5c0694","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 28 Oct 2024 08:35:39 GMT + - Tue, 29 Oct 2024 11:41:43 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml similarity index 70% rename from src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml index 1b4c6115d9..2b4cc25e02 100644 --- a/src/openforms/contrib/objects_api/tests/data/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94 + uri: http://localhost:8002/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/7cdf3889-edd2-4b65-8140-a36e96dfbb94","uuid":"7cdf3889-edd2-4b65-8140-a36e96dfbb94","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-28","endAt":null,"registrationAt":"2024-10-28","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694","uuid":"5015053e-1916-42b5-9119-ede73f5c0694","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 28 Oct 2024 08:35:39 GMT + - Tue, 29 Oct 2024 11:41:43 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/test_validators.py b/src/openforms/contrib/objects_api/tests/test_validators.py index 12e22b4ac8..992cdaedc1 100644 --- a/src/openforms/contrib/objects_api/tests/test_validators.py +++ b/src/openforms/contrib/objects_api/tests/test_validators.py @@ -16,7 +16,7 @@ from ..validators import validate_object_ownership -TEST_FILES = (Path(__file__).parent / "data").resolve() +TEST_FILES = (Path(__file__).parent / "files").resolve() @override_settings( @@ -122,6 +122,10 @@ def test_user_is_not_owner_of_object(self): side_effect=RequestException, ) def test_request_exception_when_doing_permission_check(self, mock_get_object): + """ + If the object could not be fetched due to request errors, the ownership check + should not fail + """ submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, @@ -140,16 +144,16 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): }, ) - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) - self.assertEqual( - str(cm.exception), "Could not fetch object for initial data reference" - ) + validate_object_ownership(submission) @tag("gh-4398") def test_no_objects_service_configured_raises_error( self, ): + """ + If the object could not be fetched due to misconfiguration, the ownership check + should not fail + """ submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, @@ -172,16 +176,16 @@ def test_no_objects_service_configured_raises_error( }, ) - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) - self.assertEqual( - str(cm.exception), "Could not fetch object for initial data reference" - ) + validate_object_ownership(submission) @tag("gh-4398") def test_no_backends_configured_raises_error( self, ): + """ + If the object could not be fetched due to misconfiguration, the ownership check + should not fail + """ submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, @@ -189,16 +193,16 @@ def test_no_backends_configured_raises_error( ) FormRegistrationBackendFactory.create(form=submission.form, backend="email") - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) - self.assertEqual( - str(cm.exception), "Could not fetch object for initial data reference" - ) + validate_object_ownership(submission) @tag("gh-4398") def test_backend_without_options_raises_error( self, ): + """ + If the object could not be fetched due to misconfiguration, the ownership check + should not fail + """ submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, @@ -209,8 +213,4 @@ def test_backend_without_options_raises_error( form=submission.form, backend="objects_api", options={} ) - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) - self.assertEqual( - str(cm.exception), "Could not fetch object for initial data reference" - ) + validate_object_ownership(submission) diff --git a/src/openforms/contrib/objects_api/validators.py b/src/openforms/contrib/objects_api/validators.py index 5c98f57684..ed7e539382 100644 --- a/src/openforms/contrib/objects_api/validators.py +++ b/src/openforms/contrib/objects_api/validators.py @@ -130,7 +130,13 @@ def validate_object_ownership(submission: Submission) -> None: ) if not object: - raise PermissionDenied("Could not fetch object for initial data reference") + # If the object cannot be found, we cannot consider the ownership check failed + # because it is not verified that the user is not the owner + logger.info( + "Could not find object for initial_data_reference: %s", + submission.initial_data_reference, + ) + return # TODO should this path be configurable? if glom(object["record"]["data"], auth_info.attribute) != auth_info.value: From 27f7d4c02a8ebde867e0a0d7aa4ff4cc2b5e40cd Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 29 Oct 2024 13:35:21 +0100 Subject: [PATCH 05/39] :recycle: [#4398] Move ownership check to proper location in prefill code --- src/openforms/prefill/sources.py | 17 +++++-- .../prefill/tests/test_prefill_variables.py | 47 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index ef68e0be0f..3b52e44c95 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -1,6 +1,8 @@ import logging from collections import defaultdict +from django.core.exceptions import PermissionDenied + import elasticapm from rest_framework.exceptions import ValidationError from zgw_consumers.concurrent import parallel @@ -54,11 +56,6 @@ def invoke_plugin( if not plugin.is_enabled: raise PluginNotEnabled() - # If an `initial_data_reference` was passed, we must verify that the - # authenticated user is the owner of the referenced object - if submission.initial_data_reference: - plugin.verify_initial_data_ownership(submission) - attributes = [attribute for field in fields for attribute in field] try: values = plugin.get_prefill_values(submission, attributes, identifier_role) @@ -101,6 +98,16 @@ def fetch_prefill_values_from_options( values: dict[str, JSONEncodable] = {} for variable in variables: plugin = register[variable.form_variable.prefill_plugin] + + # If an `initial_data_reference` was passed, we must verify that the + # authenticated user is the owner of the referenced object + try: + if submission.initial_data_reference: + plugin.verify_initial_data_ownership(submission) + except PermissionDenied as exc: + logevent.prefill_retrieve_failure(submission, plugin, exc) + continue + options_serializer = plugin.options(data=variable.form_variable.prefill_options) try: diff --git a/src/openforms/prefill/tests/test_prefill_variables.py b/src/openforms/prefill/tests/test_prefill_variables.py index 4944c14b7a..01806007e3 100644 --- a/src/openforms/prefill/tests/test_prefill_variables.py +++ b/src/openforms/prefill/tests/test_prefill_variables.py @@ -1,7 +1,7 @@ from unittest.mock import patch from django.core.exceptions import PermissionDenied -from django.test import RequestFactory, TestCase, TransactionTestCase +from django.test import RequestFactory, TestCase, TransactionTestCase, tag import requests_mock from zgw_consumers.test.factories import ServiceFactory @@ -19,7 +19,6 @@ FormVariableFactory, ) from openforms.logging.models import TimelineLogProxy -from openforms.prefill.contrib.demo.constants import Attributes from openforms.submissions.constants import SubmissionValueVariableSources from openforms.submissions.tests.factories import ( SubmissionFactory, @@ -321,6 +320,7 @@ def test_no_success_message_on_failure(self, m, m_solo): for log in logs: self.assertNotEqual(log.event, "prefill_retrieve_success") + @tag("gh-4398") def test_verify_initial_data_ownership(self): form_step = FormStepFactory.create( form_definition__configuration={ @@ -329,15 +329,22 @@ def test_verify_initial_data_ownership(self): "type": "postcode", "key": "postcode", "inputMask": "9999 AA", - "prefill": { - "plugin": "demo", - "attribute": Attributes.random_string, - }, - "defaultValue": "", } ] } ) + FormVariableFactory.create( + key="voornamen", + form=form_step.form, + prefill_plugin="demo", + prefill_attribute="", + prefill_options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": 1, + "objecttype_version": 1, + }, + ) with self.subTest( "verify_initial_data_ownership is not called if initial_data_reference is not specified" @@ -352,9 +359,13 @@ def test_verify_initial_data_ownership(self): with patch( "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" ) as mock_verify_ownership: - prefill_variables(submission=submission_step.submission) + with patch( + "openforms.prefill.contrib.demo.plugin.DemoPrefill.get_prefill_values_from_options", + return_value={"postcode": "1234AB"}, + ): + prefill_variables(submission=submission_step.submission) - mock_verify_ownership.assert_not_called() + mock_verify_ownership.assert_not_called() logs = TimelineLogProxy.objects.filter( object_id=submission_step.submission.id @@ -377,11 +388,15 @@ def test_verify_initial_data_ownership(self): with patch( "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" ) as mock_verify_ownership: - prefill_variables(submission=submission_step.submission) + with patch( + "openforms.prefill.contrib.demo.plugin.DemoPrefill.get_prefill_values_from_options", + return_value={"postcode": "1234AB"}, + ): + prefill_variables(submission=submission_step.submission) - mock_verify_ownership.assert_called_once_with( - submission_step.submission - ) + mock_verify_ownership.assert_called_once_with( + submission_step.submission + ) logs = TimelineLogProxy.objects.filter( object_id=submission_step.submission.id @@ -405,8 +420,7 @@ def test_verify_initial_data_ownership(self): "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership", side_effect=PermissionDenied, ) as mock_verify_ownership: - with self.assertRaises(PermissionDenied): - prefill_variables(submission=submission_step.submission) + prefill_variables(submission=submission_step.submission) mock_verify_ownership.assert_called_once_with( submission_step.submission @@ -418,3 +432,6 @@ def test_verify_initial_data_ownership(self): self.assertEqual( logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) From 9dc4fffd8617e125bb4aad094d6c24fccf77eef0 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 29 Oct 2024 14:16:29 +0100 Subject: [PATCH 06/39] :sparkles: [#4398] Ensure logs from ownership check in pre-registration work --- src/openforms/logging/logevent.py | 8 +++++ .../events/object_ownership_check_failure.txt | 4 +++ src/openforms/registrations/tasks.py | 31 ++++++++++--------- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/openforms/logging/templates/logging/events/object_ownership_check_failure.txt diff --git a/src/openforms/logging/logevent.py b/src/openforms/logging/logevent.py index 2dbe321049..3a05cbad2b 100644 --- a/src/openforms/logging/logevent.py +++ b/src/openforms/logging/logevent.py @@ -247,6 +247,14 @@ def registration_attempts_limited(submission: Submission): ) +def object_ownership_check_failure(submission: Submission, plugin=None): + _create_log( + submission, + "object_ownership_check_failure", + plugin=plugin, + ) + + # - - - diff --git a/src/openforms/logging/templates/logging/events/object_ownership_check_failure.txt b/src/openforms/logging/templates/logging/events/object_ownership_check_failure.txt new file mode 100644 index 0000000000..b6f8a4be43 --- /dev/null +++ b/src/openforms/logging/templates/logging/events/object_ownership_check_failure.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% blocktrans trimmed with plugin=log.fmt_plugin lead=log.fmt_lead %} + {{ lead }}: Registration plugin {{ plugin }} reported: authenticated user is not the owner of referenced object. +{% endblocktrans %} diff --git a/src/openforms/registrations/tasks.py b/src/openforms/registrations/tasks.py index 1ac00809d1..373b381848 100644 --- a/src/openforms/registrations/tasks.py +++ b/src/openforms/registrations/tasks.py @@ -74,27 +74,30 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: ) return + registration_plugin = get_registration_plugin(submission) + + # If an `initial_data_reference` was passed, we must verify that the + # authenticated user is the owner of the referenced object + if registration_plugin and submission.initial_data_reference: + try: + registration_plugin.verify_initial_data_ownership(submission) + except PermissionDenied as e: + logger.exception( + "Submission with initial_data_reference did not pass ownership check for plugin %s", + registration_plugin.verbose_name, + ) + logevent.object_ownership_check_failure( + submission, plugin=registration_plugin + ) + raise e + with transaction.atomic(): - registration_plugin = get_registration_plugin(submission) if not registration_plugin: set_submission_reference(submission) submission.pre_registration_completed = True submission.save() return - # If an `initial_data_reference` was passed, we must verify that the - # authenticated user is the owner of the referenced object - if submission.initial_data_reference: - try: - registration_plugin.verify_initial_data_ownership(submission) - except PermissionDenied as e: - logger.exception( - "Submission with initial_data_reference did not pass ownership check for plugin %s", - registration_plugin.verbose_name, - ) - logevent.registration_failure(submission, e, plugin=registration_plugin) - raise e - options_serializer = registration_plugin.configuration_options( data=submission.registration_backend.options, context={"validate_business_logic": False}, From f8505ead52ed1ab71f050eb73589c547c45ad16c Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 29 Oct 2024 14:40:14 +0100 Subject: [PATCH 07/39] :white_check_mark: [#4398] Fix VCR issue in Objects API ownership tests --- ...ests.test_user_is_not_owner_of_object.yaml | 6 +- ...torTests.test_user_is_owner_of_object.yaml | 6 +- .../setUpTestData.yaml | 58 +++++++++++++++++++ .../objects_api/tests/test_validators.py | 30 ++++++---- 4 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml index 2b4cc25e02..332c63e9ec 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694 + uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694","uuid":"5015053e-1916-42b5-9119-ede73f5c0694","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 29 Oct 2024 11:41:43 GMT + - Tue, 29 Oct 2024 13:38:52 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml index 2b4cc25e02..332c63e9ec 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694 + uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/5015053e-1916-42b5-9119-ede73f5c0694","uuid":"5015053e-1916-42b5-9119-ede73f5c0694","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 29 Oct 2024 11:41:43 GMT + - Tue, 29 Oct 2024 13:38:52 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml new file mode 100644 index 0000000000..4b65c9a456 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"bsn": "111222333", "foo": "bar"}, "startAt": + "2024-10-29"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '194' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '422' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 29 Oct 2024 13:38:51 GMT + Location: + - http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/test_validators.py b/src/openforms/contrib/objects_api/tests/test_validators.py index 992cdaedc1..62aaf230d4 100644 --- a/src/openforms/contrib/objects_api/tests/test_validators.py +++ b/src/openforms/contrib/objects_api/tests/test_validators.py @@ -5,6 +5,7 @@ from django.test import TestCase, override_settings, tag from requests.exceptions import RequestException +from vcr.config import VCR from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client @@ -34,16 +35,25 @@ def setUpTestData(cls): cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( for_test_docker_compose=True ) - # TODO fix tests failing after first run - with get_objects_client(cls.objects_api_group_used) as client: - object = client.create_object( - record_data=prepare_data_for_registration( - data={"bsn": "111222333", "foo": "bar"}, - objecttype_version=1, - ), - objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", - ) - cls.object_ref = object["uuid"] + + # Explicitly define a cassette for Object creation, because running this in + # setUpTestData doesn't record cassettes by default + cassette_path = Path( + cls.VCR_TEST_FILES + / "vcr_cassettes" + / cls.__qualname__ + / "setUpTestData.yaml" + ) + with VCR().use_cassette(cassette_path): + with get_objects_client(cls.objects_api_group_used) as client: + object = client.create_object( + record_data=prepare_data_for_registration( + data={"bsn": "111222333", "foo": "bar"}, + objecttype_version=1, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + ) + cls.object_ref = object["uuid"] @tag("gh-4398") def test_user_is_owner_of_object(self): From 3569555e14c567c15c477a85f632aa8201587e80 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 26 Nov 2024 13:22:56 +0100 Subject: [PATCH 08/39] :white_check_mark: [#4398] Fix failing tests due to new ownership check --- .../prefill/contrib/objects_api/tests/test_prefill.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py index 29fdadea98..5fed373f62 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py @@ -3,6 +3,7 @@ from rest_framework.test import APITestCase +from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.helpers import prepare_data_for_registration from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory @@ -47,6 +48,7 @@ def test_prefill_values_happy_flow(self): data={ "name": {"last.name": "My last name"}, "age": 45, + "bsn": "111222333", }, objecttype_version=3, ), @@ -54,6 +56,8 @@ def test_prefill_values_happy_flow(self): ) submission = SubmissionFactory.from_components( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, initial_data_reference=created_obj["uuid"], components_list=[ { @@ -146,6 +150,8 @@ def test_prefill_values_when_reference_returns_empty_values(self): ) submission = SubmissionFactory.from_components( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, initial_data_reference=created_obj["uuid"], components_list=[ { From 17358badcc0a7fa83c32da09225684a8b756929f Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 4 Nov 2024 15:27:19 +0100 Subject: [PATCH 09/39] :ok_hand: [#4398] Process PR feedback * move initial_data_reference ownership check to separate file * change `validate_object_ownership` signature to accept an ObjectsClient and create the client in the calling code --- .../objects_api/ownership_validation.py | 56 +++++ ..._backend_without_options_raises_error.yaml | 50 ++++ ...t_no_backends_configured_raises_error.yaml | 50 ++++ ...dators.py => test_ownership_validation.py} | 56 ++--- .../contrib/objects_api/validators.py | 65 +---- src/openforms/prefill/base.py | 25 +- .../prefill/contrib/objects_api/plugin.py | 24 +- ...ts.test_verify_initial_data_ownership.yaml | 50 ++++ .../setUpTestData.yaml | 58 +++++ .../test_initial_data_ownership_validation.py | 226 ++++++++++++++++++ src/openforms/prefill/sources.py | 4 +- .../prefill/tests/test_prefill_variables.py | 6 +- src/openforms/registrations/base.py | 2 +- .../contrib/objects_api/plugin.py | 19 +- .../test_initial_data_ownership_validation.py | 123 ++++++++++ 15 files changed, 687 insertions(+), 127 deletions(-) create mode 100644 src/openforms/contrib/objects_api/ownership_validation.py create mode 100644 src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml create mode 100644 src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml rename src/openforms/contrib/objects_api/tests/{test_validators.py => test_ownership_validation.py} (81%) create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml create mode 100644 src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py create mode 100644 src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py new file mode 100644 index 0000000000..17a2244364 --- /dev/null +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied + +from glom import Path, glom +from requests.exceptions import RequestException + +from openforms.contrib.objects_api.clients import ObjectsClient + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from openforms.submissions.models import Submission + + +def validate_object_ownership( + submission: Submission, + client: ObjectsClient, + object_attribute: list[str] | Path, +) -> None: + """ + Function to check whether the user associated with a Submission is the owner + of an Object in the Objects API, by comparing the authentication attribute. + + This validation should only be done if the Submission has an `initial_data_reference` + """ + assert submission.initial_data_reference + + try: + auth_info = submission.auth_info + except ObjectDoesNotExist: + raise PermissionDenied("Cannot pass data reference as anonymous user") + + object = None + try: + object = client.get_object(submission.initial_data_reference) + except RequestException: + logger.exception( + "Something went wrong while trying to retrieve " + "object for initial_data_reference" + ) + + if not object: + # If the object cannot be found, we cannot consider the ownership check failed + # because it is not verified that the user is not the owner + logger.info( + "Could not find object for initial_data_reference: %s", + submission.initial_data_reference, + ) + return + + if glom(object["record"]["data"], *object_attribute) != auth_info.value: + raise PermissionDenied("User is not the owner of the referenced object") diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml new file mode 100644 index 0000000000..ed0647d985 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '422' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 12:48:04 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml new file mode 100644 index 0000000000..ed0647d985 --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '422' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 12:48:04 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/test_validators.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py similarity index 81% rename from src/openforms/contrib/objects_api/tests/test_validators.py rename to src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 62aaf230d4..13fe750188 100644 --- a/src/openforms/contrib/objects_api/tests/test_validators.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -15,7 +15,7 @@ from openforms.submissions.tests.factories import SubmissionFactory from openforms.utils.tests.vcr import OFVCRMixin -from ..validators import validate_object_ownership +from ..ownership_validation import validate_object_ownership TEST_FILES = (Path(__file__).parent / "files").resolve() @@ -88,14 +88,16 @@ def test_user_is_owner_of_object(self): }, ) - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"]) @tag("gh-4398") def test_permission_denied_if_user_is_not_logged_in(self): submission = SubmissionFactory.create(initial_data_reference=self.object_ref) - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission, client, ["bsn"]) self.assertEqual( str(cm.exception), "Cannot pass data reference as anonymous user" ) @@ -120,8 +122,9 @@ def test_user_is_not_owner_of_object(self): }, ) - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission, client, ["bsn"]) self.assertEqual( str(cm.exception), "User is not the owner of the referenced object" ) @@ -154,39 +157,8 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): }, ) - validate_object_ownership(submission) - - @tag("gh-4398") - def test_no_objects_service_configured_raises_error( - self, - ): - """ - If the object could not be fetched due to misconfiguration, the ownership check - should not fail - """ - submission = SubmissionFactory.create( - auth_info__value="111222333", - auth_info__attribute=AuthAttribute.bsn, - initial_data_reference=self.object_ref, - ) - - objects_api_group_used = ObjectsAPIGroupConfigFactory.create( - objects_service=None, - ) - - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"]) @tag("gh-4398") def test_no_backends_configured_raises_error( @@ -203,7 +175,8 @@ def test_no_backends_configured_raises_error( ) FormRegistrationBackendFactory.create(form=submission.form, backend="email") - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"]) @tag("gh-4398") def test_backend_without_options_raises_error( @@ -223,4 +196,5 @@ def test_backend_without_options_raises_error( form=submission.form, backend="objects_api", options={} ) - validate_object_ownership(submission) + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"]) diff --git a/src/openforms/contrib/objects_api/validators.py b/src/openforms/contrib/objects_api/validators.py index ed7e539382..cde3d2a987 100644 --- a/src/openforms/contrib/objects_api/validators.py +++ b/src/openforms/contrib/objects_api/validators.py @@ -1,20 +1,11 @@ from __future__ import annotations -import logging import warnings from functools import partial -from typing import TYPE_CHECKING -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError +from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from glom import glom -from requests.exceptions import RequestException - -from openforms.contrib.objects_api.clients import ( - NoServiceConfigured, - get_objects_client, -) from openforms.contrib.zgw.clients.catalogi import Catalogus from openforms.contrib.zgw.validators import ( validate_catalogue_reference as _validate_catalogue_reference, @@ -23,11 +14,6 @@ from .clients import get_catalogi_client from .models import ObjectsAPIGroupConfig -logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from openforms.submissions.models import Submission - def validate_catalogue_reference( config: ObjectsAPIGroupConfig, @@ -92,52 +78,3 @@ def validate_document_type_references( if errors: raise ValidationError(errors) - - -def validate_object_ownership(submission: Submission) -> None: - form = submission.form - - try: - auth_info = submission.auth_info - except ObjectDoesNotExist: - raise PermissionDenied("Cannot pass data reference as anonymous user") - - object = None - for backend in form.registration_backends.filter(backend="objects_api"): - if not backend.options: - continue - - api_group = ObjectsAPIGroupConfig.objects.filter( - pk=backend.options.get("objects_api_group") - ).first() - if not api_group: - continue - - try: - with get_objects_client(api_group) as client: - try: - object = client.get_object(submission.initial_data_reference) - break - except RequestException: - logger.exception( - "Something went wrong while trying to retrieve " - "object for initial_data_reference" - ) - except NoServiceConfigured: - logger.exception( - "Something went wrong while trying to create a client " - "for Objects API" - ) - - if not object: - # If the object cannot be found, we cannot consider the ownership check failed - # because it is not verified that the user is not the owner - logger.info( - "Could not find object for initial_data_reference: %s", - submission.initial_data_reference, - ) - return - - # TODO should this path be configurable? - if glom(object["record"]["data"], auth_info.attribute) != auth_info.value: - raise PermissionDenied("User is not the owner of the referenced object") diff --git a/src/openforms/prefill/base.py b/src/openforms/prefill/base.py index eaa3ba8aa2..408f11d1c8 100644 --- a/src/openforms/prefill/base.py +++ b/src/openforms/prefill/base.py @@ -65,6 +65,20 @@ def get_prefill_values( """ raise NotImplementedError("You must implement the 'get_prefill_values' method.") + def verify_initial_data_ownership( + self, submission: Submission, prefill_options: dict + ) -> None: + """ + Hook to check if the authenticated user is the owner of the object + referenced to by `initial_data_reference` + + :param submission: an active :class:`Submission` instance + :param prefill_options: a dictionary containing the configuration options + """ + raise NotImplementedError( + "You must implement the 'verify_initial_data_ownership' method." + ) + @classmethod def get_prefill_values_from_options( cls, @@ -129,14 +143,3 @@ def get_identifier_value( @classmethod def configuration_context(cls) -> JSONObject | None: return None - - def verify_initial_data_ownership(self, submission: Submission) -> None: - """ - Hook to check if the authenticated user is the owner of the object - referenced to by `initial_data_reference - - :param submission: an active :class:`Submission` instance - """ - raise NotImplementedError( - "You must implement the 'verify_initial_data_ownership' method." - ) diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index 019a2dfe8b..42c919d12b 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -8,7 +8,7 @@ from openforms.contrib.objects_api.checks import check_config from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig -from openforms.contrib.objects_api.validators import validate_object_ownership +from openforms.contrib.objects_api.ownership_validation import validate_object_ownership from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig from openforms.submissions.models import Submission from openforms.typing import JSONEncodable, JSONObject @@ -29,6 +29,25 @@ class ObjectsAPIPrefill(BasePlugin[ObjectsAPIOptions]): verbose_name = _("Objects API") options = ObjectsAPIOptionsSerializer + def verify_initial_data_ownership( + self, submission: Submission, prefill_options: dict + ) -> None: + api_group = ObjectsAPIGroupConfig.objects.filter( + pk=prefill_options.get("objects_api_group") + ).first() + + if not api_group: + logger.info( + "No api group found to perform initial_data_reference ownership check for submission %s with options %s", + submission, + prefill_options, + ) + return + + with get_objects_client(api_group) as client: + # TODO configurable path + validate_object_ownership(submission, client, ["bsn"]) + @classmethod def get_prefill_values_from_options( cls, @@ -81,6 +100,3 @@ def configuration_context(cls) -> JSONObject | None: for group in ObjectsAPIGroupConfig.objects.iterator() ] } - - def verify_initial_data_ownership(self, submission: Submission) -> None: - validate_object_ownership(submission) diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml new file mode 100644 index 0000000000..04ab29a804 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75","uuid":"351348b1-ff52-440f-8142-5e080b0a1b75","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 12:48:04 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml new file mode 100644 index 0000000000..0406a729e6 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, + "startAt": "2024-11-04"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '205' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75","uuid":"351348b1-ff52-440f-8142-5e080b0a1b75","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 12:48:04 GMT + Location: + - http://localhost:8002/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75 + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py new file mode 100644 index 0000000000..456daaf7f1 --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -0,0 +1,226 @@ +from pathlib import Path +from unittest.mock import patch + +from django.core.exceptions import PermissionDenied +from django.test import TestCase, tag + +from vcr.config import VCR + +from openforms.authentication.service import AuthAttribute +from openforms.contrib.objects_api.clients import get_objects_client +from openforms.contrib.objects_api.helpers import prepare_data_for_registration +from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.forms.tests.factories import ( + FormFactory, + FormRegistrationBackendFactory, + FormStepFactory, + FormVariableFactory, +) +from openforms.logging.models import TimelineLogProxy +from openforms.prefill.service import prefill_variables +from openforms.submissions.tests.factories import SubmissionStepFactory +from openforms.utils.tests.vcr import OFVCRMixin + +TEST_FILES = (Path(__file__).parent / "files").resolve() + + +class ObjectsAPIPrefillDataOwnershipCheckTests(OFVCRMixin, TestCase): + VCR_TEST_FILES = TEST_FILES + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + + # Explicitly define a cassette for Object creation, because running this in + # setUpTestData doesn't record cassettes by default + cassette_path = Path( + cls.VCR_TEST_FILES + / "vcr_cassettes" + / cls.__qualname__ + / "setUpTestData.yaml" + ) + with VCR().use_cassette(cassette_path): + with get_objects_client(cls.objects_api_group_used) as client: + object = client.create_object( + record_data=prepare_data_for_registration( + data={"bsn": "111222333", "some": {"path": "foo"}}, + objecttype_version=1, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + ) + cls.object_ref = object["uuid"] + + @tag("gh-4398") + def test_verify_initial_data_ownership(self): + objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() + + form = FormFactory.create() + # An objects API backend with a different API group + FormRegistrationBackendFactory.create( + form=form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_unused.pk, + "objecttype_version": 1, + }, + ) + # Another backend that should be ignored + FormRegistrationBackendFactory.create(form=form, backend="email") + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + form_step = FormStepFactory.create( + form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + } + ] + } + ) + variable = FormVariableFactory.create( + key="voornamen", + form=form_step.form, + prefill_plugin="objects_api", + prefill_attribute="", + prefill_options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_used.pk, + "objecttype_version": 1, + "variables_mapping": [ + {"variable_key": "voornamen", "target_path": ["some", "path"]}, + ], + }, + ) + + with self.subTest( + "verify_initial_data_ownership is called if initial_data_reference is specified" + ): + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) + + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership" + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) + + self.assertEqual(mock_validate_object_ownership.call_count, 1) + + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + + self.assertEqual(call.args[0], submission_step.submission) + self.assertEqual( + call.args[1].base_url, + objects_api_group_used.objects_service.api_root, + ) + self.assertEqual(call.args[2], ["bsn"]) + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 + ) + + with self.subTest( + "verify_initial_data_ownership raising error causes prefill to fail" + ): + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) + + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) + + self.assertEqual(mock_validate_object_ownership.call_count, 1) + + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + + self.assertEqual(call.args[0], submission_step.submission) + self.assertEqual( + call.args[1].base_url, + objects_api_group_used.objects_service.api_root, + ) + self.assertEqual(call.args[2], ["bsn"]) + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) + + with self.subTest( + "verify_initial_data_ownership does not raise errors if no API group is found" + ): + variable.prefill_options["objects_api_group"] = ( + ObjectsAPIGroupConfig.objects.last().pk + 1 + ) + variable.save() + submission_step = SubmissionStepFactory.create( + submission__form=form_step.form, + form_step=form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) + + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) + + self.assertEqual(mock_validate_object_ownership.call_count, 0) + + logs = TimelineLogProxy.objects.filter( + object_id=submission_step.submission.id + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) + # Prefilling fails, because the API group does not exist + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index 3b52e44c95..7a98ae136b 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -103,7 +103,9 @@ def fetch_prefill_values_from_options( # authenticated user is the owner of the referenced object try: if submission.initial_data_reference: - plugin.verify_initial_data_ownership(submission) + plugin.verify_initial_data_ownership( + submission, variable.form_variable.prefill_options + ) except PermissionDenied as exc: logevent.prefill_retrieve_failure(submission, plugin, exc) continue diff --git a/src/openforms/prefill/tests/test_prefill_variables.py b/src/openforms/prefill/tests/test_prefill_variables.py index 01806007e3..818c6f23a7 100644 --- a/src/openforms/prefill/tests/test_prefill_variables.py +++ b/src/openforms/prefill/tests/test_prefill_variables.py @@ -333,7 +333,7 @@ def test_verify_initial_data_ownership(self): ] } ) - FormVariableFactory.create( + variable = FormVariableFactory.create( key="voornamen", form=form_step.form, prefill_plugin="demo", @@ -395,7 +395,7 @@ def test_verify_initial_data_ownership(self): prefill_variables(submission=submission_step.submission) mock_verify_ownership.assert_called_once_with( - submission_step.submission + submission_step.submission, variable.prefill_options ) logs = TimelineLogProxy.objects.filter( @@ -423,7 +423,7 @@ def test_verify_initial_data_ownership(self): prefill_variables(submission=submission_step.submission) mock_verify_ownership.assert_called_once_with( - submission_step.submission + submission_step.submission, variable.prefill_options ) logs = TimelineLogProxy.objects.filter( diff --git a/src/openforms/registrations/base.py b/src/openforms/registrations/base.py index c68f3d813d..14804549bf 100644 --- a/src/openforms/registrations/base.py +++ b/src/openforms/registrations/base.py @@ -88,7 +88,7 @@ def get_variables(self) -> list[FormVariable]: def verify_initial_data_ownership(self, submission: Submission) -> None: """ Hook to check if the authenticated user is the owner of the object - referenced to by `initial_data_reference + referenced to by `initial_data_reference` :param submission: an active :class:`Submission` instance """ diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index e4437b7263..063ae20c66 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -13,7 +13,8 @@ get_objects_client, get_objecttypes_client, ) -from openforms.contrib.objects_api.validators import validate_object_ownership +from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig +from openforms.contrib.objects_api.ownership_validation import validate_object_ownership from openforms.registrations.utils import execute_unless_result_exists from openforms.variables.service import get_static_variables @@ -173,4 +174,18 @@ def get_variables(self) -> list[FormVariable]: return get_static_variables(variables_registry=variables_registry) def verify_initial_data_ownership(self, submission: Submission) -> None: - validate_object_ownership(submission) + for backend in submission.form.registration_backends.filter( + backend=self.identifier + ): + if not backend.options: + continue + + api_group = ObjectsAPIGroupConfig.objects.filter( + pk=backend.options.get("objects_api_group") + ).first() + if not api_group: + continue + + with get_objects_client(api_group) as client: + # TODO configurable path + validate_object_ownership(submission, client, ["bsn"]) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py new file mode 100644 index 0000000000..d8c819fe71 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -0,0 +1,123 @@ +from unittest.mock import patch + +from django.core.exceptions import PermissionDenied +from django.test import TestCase, tag + +from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory +from openforms.forms.tests.factories import FormFactory, FormRegistrationBackendFactory +from openforms.submissions.constants import PostSubmissionEvents +from openforms.submissions.tasks.registration import pre_registration +from openforms.submissions.tests.factories import SubmissionFactory + + +class ObjectsAPIPreRegistrationTests(TestCase): + @tag("gh-4398") + def test_verify_initial_data_ownership(self): + objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True + ) + objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() + + form = FormFactory.create() + # An objects API backend with a different API group + FormRegistrationBackendFactory.create( + form=form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_unused.pk, + "objecttype_version": 1, + }, + ) + # Another backend that should be ignored + FormRegistrationBackendFactory.create(form=form, backend="email") + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.subTest( + "verify_initial_data_ownership is not called if no initial_data_reference is specified" + ): + submission = SubmissionFactory.create( + form=form, + completed_not_preregistered=True, + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + mock_validate_object_ownership.assert_not_called() + + with self.subTest( + "verify_initial_data_ownership is called if initial_data_reference exists is specified" + ): + submission = SubmissionFactory.create( + form=form, + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership" + ) as mock_validate_object_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + self.assertEqual(mock_validate_object_ownership.call_count, 2) + + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call1, call2 = mock_validate_object_ownership.mock_calls + + self.assertEqual(call1.args[0], submission) + self.assertEqual( + call1.args[1].base_url, + objects_api_group_unused.objects_service.api_root, + ) + self.assertEqual(call1.args[2], ["bsn"]) + + self.assertEqual(call2.args[0], submission) + self.assertEqual( + call2.args[1].base_url, + objects_api_group_used.objects_service.api_root, + ) + self.assertEqual(call2.args[2], ["bsn"]) + + with self.subTest( + "verify_initial_data_ownership raising error causes pre registration to fail" + ): + submission = SubmissionFactory.create( + form=form, + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + with self.assertRaises(PermissionDenied): + pre_registration(submission.id, PostSubmissionEvents.on_completion) + self.assertEqual(mock_validate_object_ownership.call_count, 1) + + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + + self.assertEqual(call.args[0], submission) + self.assertEqual( + call.args[1].base_url, + objects_api_group_unused.objects_service.api_root, + ) + self.assertEqual(call.args[2], ["bsn"]) From 0b600717689177c2484b58ad4933db4b8f92b675 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 4 Nov 2024 16:51:07 +0100 Subject: [PATCH 10/39] :sparkles: [#4398] Configurable path to auth attribute for ownership check --- .../objects_api/ownership_validation.py | 7 +- ...owner_of_object_nested_auth_attribute.yaml | 106 ++++++++++++++++++ .../tests/test_ownership_validation.py | 37 ++++++ .../objectsapi/LegacyConfigFields.js | 2 + .../objectsapi/ObjectsApiOptionsForm.js | 1 + .../objectsapi/V2ConfigFields.js | 2 + .../objectsapi/fields/AuthAttributePath.js | 52 +++++++++ .../registrations/objectsapi/fields/index.js | 1 + .../prefill/contrib/objects_api/plugin.py | 11 +- .../test_initial_data_ownership_validation.py | 5 +- .../contrib/objects_api/config.py | 8 ++ .../contrib/objects_api/plugin.py | 11 +- .../test_initial_data_ownership_validation.py | 15 ++- .../contrib/objects_api/typing.py | 1 + 14 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml create mode 100644 src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py index 17a2244364..ac31c9b056 100644 --- a/src/openforms/contrib/objects_api/ownership_validation.py +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -19,7 +19,7 @@ def validate_object_ownership( submission: Submission, client: ObjectsClient, - object_attribute: list[str] | Path, + object_attribute: list[str], ) -> None: """ Function to check whether the user associated with a Submission is the owner @@ -52,5 +52,8 @@ def validate_object_ownership( ) return - if glom(object["record"]["data"], *object_attribute) != auth_info.value: + if ( + glom(object["record"]["data"], Path(*object_attribute), default=None) + != auth_info.value + ): raise PermissionDenied("User is not the owner of the referenced object") diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml new file mode 100644 index 0000000000..4f59403dbe --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml @@ -0,0 +1,106 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"nested": {"bsn": "111222333"}, "foo": + "bar"}, "startAt": "2024-11-04"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '206' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d","uuid":"9425a239-d592-4e02-b5dd-69609fdc292d","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 15:49:09 GMT + Location: + - http://localhost:8002/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d","uuid":"9425a239-d592-4e02-b5dd-69609fdc292d","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Mon, 04 Nov 2024 15:49:09 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 13fe750188..6cb8af0f7b 100644 --- a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -129,6 +129,43 @@ def test_user_is_not_owner_of_object(self): str(cm.exception), "User is not the owner of the referenced object" ) + @tag("gh-4398") + def test_user_is_not_owner_of_object_nested_auth_attribute(self): + with get_objects_client(self.objects_api_group_used) as client: + object = client.create_object( + record_data=prepare_data_for_registration( + data={"nested": {"bsn": "111222333"}, "foo": "bar"}, + objecttype_version=1, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + ) + object_ref = object["uuid"] + + submission = SubmissionFactory.create( + auth_info__value="123456782", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=object_ref, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission, client, ["nested", "bsn"]) + self.assertEqual( + str(cm.exception), "User is not the owner of the referenced object" + ) + @tag("gh-4398") @patch( "openforms.contrib.objects_api.clients.objects.ObjectsClient.get_object", diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js index 8d277e1c4f..4e4f8a18dd 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js @@ -14,6 +14,7 @@ import { import ErrorBoundary from 'components/errors/ErrorBoundary'; import { + AuthAttributePath, DocumentTypesFieldet, LegacyDocumentTypesFieldet, OrganisationRSIN, @@ -108,6 +109,7 @@ const LegacyConfigFields = ({apiGroupChoices}) => ( > + diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js index e68c0508a6..b84e8bcff8 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js @@ -56,6 +56,7 @@ ObjectsApiOptionsForm.propTypes = { objecttype: PropTypes.string, objecttypeVersion: PropTypes.number, updateExistingObject: PropTypes.bool, + authAttributePath: PropTypes.bool, productaanvraagType: PropTypes.string, informatieobjecttypeSubmissionReport: PropTypes.string, uploadSubmissionCsv: PropTypes.bool, diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js index a65c8a0717..2b6efed72b 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js @@ -12,6 +12,7 @@ import { import ErrorBoundary from 'components/errors/ErrorBoundary'; import { + AuthAttributePath, DocumentTypesFieldet, LegacyDocumentTypesFieldet, OrganisationRSIN, @@ -135,6 +136,7 @@ const V2ConfigFields = ({apiGroupChoices}) => { > + diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js new file mode 100644 index 0000000000..55c2e1e523 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js @@ -0,0 +1,52 @@ +import {useField} from 'formik'; +import {useContext} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import {FeatureFlagsContext} from 'components/admin/form_design/Context'; +import ArrayInput from 'components/admin/forms/ArrayInput'; +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {Checkbox} from 'components/admin/forms/Inputs'; + +const AuthAttributePath = () => { + const [fieldProps, , fieldHelpers] = useField({name: 'authAttributePath', type: 'array'}); + const {setValue} = fieldHelpers; + const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = + useContext(FeatureFlagsContext); + + if (!REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION) { + return null; + } + + return ( + + + } + helpText={ + + } + > + { + setValue(value); + }} + inputType="text" + /> + + + ); +}; + +AuthAttributePath.propTypes = {}; + +export default AuthAttributePath; diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js index bc81b78752..eb718ea817 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js @@ -3,3 +3,4 @@ export {DocumentTypesFieldet} from './DocumentTypes'; export {default as UpdateExistingObject} from './UpdateExistingObject'; export {default as UploadSubmissionCsv} from './UploadSubmissionCSV'; export {default as OrganisationRSIN} from './OrganisationRSIN'; +export {default as AuthAttributePath} from './AuthAttributePath'; diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index 42c919d12b..8d88ea3e54 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -44,9 +44,16 @@ def verify_initial_data_ownership( ) return + auth_attribute_path = prefill_options.get("auth_attribute_path") + if not auth_attribute_path: + logger.info( + "Cannot perform initial data ownership check, because `auth_attribute_path` is missing from %s", + prefill_options, + ) + return + with get_objects_client(api_group) as client: - # TODO configurable path - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, auth_attribute_path) @classmethod def get_prefill_values_from_options( diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 456daaf7f1..bc1e68beba 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -109,6 +109,7 @@ def test_verify_initial_data_ownership(self): "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", "objects_api_group": objects_api_group_used.pk, "objecttype_version": 1, + "auth_attribute_path": ["nested", "bsn"], "variables_mapping": [ {"variable_key": "voornamen", "target_path": ["some", "path"]}, ], @@ -142,7 +143,7 @@ def test_verify_initial_data_ownership(self): call.args[1].base_url, objects_api_group_used.objects_service.api_root, ) - self.assertEqual(call.args[2], ["bsn"]) + self.assertEqual(call.args[2], ["nested", "bsn"]) logs = TimelineLogProxy.objects.filter( object_id=submission_step.submission.id @@ -180,7 +181,7 @@ def test_verify_initial_data_ownership(self): call.args[1].base_url, objects_api_group_used.objects_service.api_root, ) - self.assertEqual(call.args[2], ["bsn"]) + self.assertEqual(call.args[2], ["nested", "bsn"]) logs = TimelineLogProxy.objects.filter( object_id=submission_step.submission.id diff --git a/src/openforms/registrations/contrib/objects_api/config.py b/src/openforms/registrations/contrib/objects_api/config.py index cb3ab09d3a..729ef073cd 100644 --- a/src/openforms/registrations/contrib/objects_api/config.py +++ b/src/openforms/registrations/contrib/objects_api/config.py @@ -114,6 +114,14 @@ class ObjectsAPIOptionsSerializer(JsonSchemaSerializerMixin, serializers.Seriali ), default=False, ) + auth_attribute_path = serializers.ListField( + child=serializers.CharField(label=_("Segment of a JSON path")), + label=_("Path to auth attribute (e.g. BSN/KVK) in objects"), + help_text=_( + "This is used to perform validation to verify that the authenticated user is the owner of the object." + ), + default=list, + ) upload_submission_csv = serializers.BooleanField( label=_("Upload submission CSV"), help_text=_( diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 063ae20c66..9821892841 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -186,6 +186,13 @@ def verify_initial_data_ownership(self, submission: Submission) -> None: if not api_group: continue + auth_attribute_path = backend.options.get("auth_attribute_path") + if not auth_attribute_path: + logger.info( + "Cannot perform initial data ownership check, because backend %s has no `auth_attribute_path` configured", + backend, + ) + continue + with get_objects_client(api_group) as client: - # TODO configurable path - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, auth_attribute_path) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py index d8c819fe71..3603959c20 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -19,6 +19,17 @@ def test_verify_initial_data_ownership(self): objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() form = FormFactory.create() + # An objects API backend that is missing `auth_attribute_path` + FormRegistrationBackendFactory.create( + form=form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": objects_api_group_unused.pk, + "objecttype_version": 1, + }, + ) # An objects API backend with a different API group FormRegistrationBackendFactory.create( form=form, @@ -28,6 +39,7 @@ def test_verify_initial_data_ownership(self): "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", "objects_api_group": objects_api_group_unused.pk, "objecttype_version": 1, + "auth_attribute_path": ["bsn"], }, ) # Another backend that should be ignored @@ -41,6 +53,7 @@ def test_verify_initial_data_ownership(self): "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", "objects_api_group": objects_api_group_used.pk, "objecttype_version": 1, + "auth_attribute_path": ["nested", "bsn"], }, ) @@ -92,7 +105,7 @@ def test_verify_initial_data_ownership(self): call2.args[1].base_url, objects_api_group_used.objects_service.api_root, ) - self.assertEqual(call2.args[2], ["bsn"]) + self.assertEqual(call2.args[2], ["nested", "bsn"]) with self.subTest( "verify_initial_data_ownership raising error causes pre registration to fail" diff --git a/src/openforms/registrations/contrib/objects_api/typing.py b/src/openforms/registrations/contrib/objects_api/typing.py index 409c7460e2..c18a545f0d 100644 --- a/src/openforms/registrations/contrib/objects_api/typing.py +++ b/src/openforms/registrations/contrib/objects_api/typing.py @@ -26,6 +26,7 @@ class _BaseRegistrationOptions(TypedDict, total=False): objecttype: Required[UUID] objecttype_version: Required[int] update_existing_object: Required[bool] + auth_attribute_path: list[str] # metadata of documents created in the documents API upload_submission_csv: bool From d9ab954d30d57678caaa1c0a3169ea5c67df6a39 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 5 Nov 2024 12:33:55 +0100 Subject: [PATCH 11/39] :fire: [#4398] Remove authentication view code to pass initial_data_reference it is no longer needed because the query parameter is now added to the nextUrl via the SDK --- src/openforms/authentication/views.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/openforms/authentication/views.py b/src/openforms/authentication/views.py index 060033108b..d1cb20bbd9 100644 --- a/src/openforms/authentication/views.py +++ b/src/openforms/authentication/views.py @@ -166,11 +166,6 @@ class AuthenticationStartView(AuthenticationFlowBaseView): register = register def get(self, request: Request, slug: str, plugin_id: str): - # Store the initial data reference in the session, so it can be passed back - # in the `AuthenticationReturnView` - request.session["initial_data_reference"] = request.query_params.get( - "initial_data_reference" - ) form = self.get_object() try: plugin = self.register[plugin_id] @@ -305,7 +300,6 @@ def _handle_return(self, request: Request, slug: str, plugin_id: str): plugin. We must define ``get`` and ``post`` to have them properly show up and be documented in the OAS. """ - initial_data_reference = request.session.pop("initial_data_reference", None) form = self.get_object() try: plugin = self.register[plugin_id] @@ -344,15 +338,6 @@ def _handle_return(self, request: Request, slug: str, plugin_id: str): if hasattr(request, "session") and FORM_AUTH_SESSION_KEY in request.session: authentication_success.send(sender=self.__class__, request=request) - - if initial_data_reference: - # Pass the initial data reference back to the SDK, to allow sending it - # with the submission create call - return HttpResponseRedirect( - furl(response.url) - .add({"initial_data_reference": initial_data_reference}) - .url - ) return response def _handle_co_sign(self, form: Form, plugin: BasePlugin) -> None: From 191d09e8de0b531231c1ce86efb78f5b8bdca746 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 5 Nov 2024 14:25:37 +0100 Subject: [PATCH 12/39] :loud_sound: [#4398] Add logevents for object ownership check and raise errors if the plugin options is missing `auth_attribute_path` --- .../objects_api/ownership_validation.py | 18 ++++++++++++ src/openforms/logging/logevent.py | 24 ++++++++++++++++ .../object_ownership_check_anonymous_user.txt | 4 +++ ..._ownership_check_improperly_configured.txt | 4 +++ .../events/object_ownership_check_success.txt | 4 +++ .../prefill/contrib/objects_api/plugin.py | 11 ++++++-- .../contrib/objects_api/tests/test_prefill.py | 28 +++++++++++++------ src/openforms/prefill/sources.py | 4 +-- .../contrib/objects_api/plugin.py | 13 +++++++-- src/openforms/registrations/tasks.py | 13 +-------- 10 files changed, 96 insertions(+), 27 deletions(-) create mode 100644 src/openforms/logging/templates/logging/events/object_ownership_check_anonymous_user.txt create mode 100644 src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt create mode 100644 src/openforms/logging/templates/logging/events/object_ownership_check_success.txt diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py index ac31c9b056..34729843d3 100644 --- a/src/openforms/contrib/objects_api/ownership_validation.py +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -9,10 +9,15 @@ from requests.exceptions import RequestException from openforms.contrib.objects_api.clients import ObjectsClient +from openforms.logging import logevent logger = logging.getLogger(__name__) if TYPE_CHECKING: + from openforms.prefill.contrib.objects_api.plugin import ObjectsAPIPrefill + from openforms.registrations.contrib.objects_api.plugin import ( + ObjectsAPIRegistration, + ) from openforms.submissions.models import Submission @@ -20,6 +25,7 @@ def validate_object_ownership( submission: Submission, client: ObjectsClient, object_attribute: list[str], + plugin: ObjectsAPIPrefill | ObjectsAPIRegistration, ) -> None: """ Function to check whether the user associated with a Submission is the owner @@ -32,6 +38,11 @@ def validate_object_ownership( try: auth_info = submission.auth_info except ObjectDoesNotExist: + logger.exception( + "Cannot perform object ownership validation for reference %s with unauthenticated user", + submission.initial_data_reference, + ) + logevent.object_ownership_check_anonymous_user(submission, plugin=plugin) raise PermissionDenied("Cannot pass data reference as anonymous user") object = None @@ -56,4 +67,11 @@ def validate_object_ownership( glom(object["record"]["data"], Path(*object_attribute), default=None) != auth_info.value ): + logger.exception( + "Submission with initial_data_reference did not pass ownership check for reference %s", + submission.initial_data_reference, + ) + logevent.object_ownership_check_failure(submission, plugin=plugin) raise PermissionDenied("User is not the owner of the referenced object") + + logevent.object_ownership_check_success(submission, plugin=plugin) diff --git a/src/openforms/logging/logevent.py b/src/openforms/logging/logevent.py index 3a05cbad2b..bdb3e3251c 100644 --- a/src/openforms/logging/logevent.py +++ b/src/openforms/logging/logevent.py @@ -255,6 +255,30 @@ def object_ownership_check_failure(submission: Submission, plugin=None): ) +def object_ownership_check_success(submission: Submission, plugin=None): + _create_log( + submission, + "object_ownership_check_success", + plugin=plugin, + ) + + +def object_ownership_check_anonymous_user(submission: Submission, plugin=None): + _create_log( + submission, + "object_ownership_check_anonymous_user", + plugin=plugin, + ) + + +def object_ownership_check_improperly_configured(submission: Submission, plugin=None): + _create_log( + submission, + "object_ownership_check_improperly_configured", + plugin=plugin, + ) + + # - - - diff --git a/src/openforms/logging/templates/logging/events/object_ownership_check_anonymous_user.txt b/src/openforms/logging/templates/logging/events/object_ownership_check_anonymous_user.txt new file mode 100644 index 0000000000..6e8b0045dc --- /dev/null +++ b/src/openforms/logging/templates/logging/events/object_ownership_check_anonymous_user.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% blocktrans trimmed with plugin=log.fmt_plugin lead=log.fmt_lead %} + {{ lead }}: Registration plugin {{ plugin }} reported: cannot perform initial data reference ownership check for anonymous user. +{% endblocktrans %} diff --git a/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt b/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt new file mode 100644 index 0000000000..204868fde4 --- /dev/null +++ b/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% blocktrans trimmed with plugin=log.fmt_plugin lead=log.fmt_lead %} + {{ lead }}: Registration plugin {{ plugin }} reported: cannot perform initial data reference ownership check due to missing `Path to auth attribute (e.g. BSN/KVK) in objects` in configuration. +{% endblocktrans %} diff --git a/src/openforms/logging/templates/logging/events/object_ownership_check_success.txt b/src/openforms/logging/templates/logging/events/object_ownership_check_success.txt new file mode 100644 index 0000000000..da43085567 --- /dev/null +++ b/src/openforms/logging/templates/logging/events/object_ownership_check_success.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% blocktrans trimmed with plugin=log.fmt_plugin lead=log.fmt_lead %} + {{ lead }}: Registration plugin {{ plugin }} reported: authenticated user is the owner of referenced object. +{% endblocktrans %} diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index 8d88ea3e54..9e12a299bb 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -1,5 +1,6 @@ import logging +from django.core.exceptions import ImproperlyConfigured from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -9,6 +10,7 @@ from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig from openforms.contrib.objects_api.ownership_validation import validate_object_ownership +from openforms.logging import logevent from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig from openforms.submissions.models import Submission from openforms.typing import JSONEncodable, JSONObject @@ -50,10 +52,15 @@ def verify_initial_data_ownership( "Cannot perform initial data ownership check, because `auth_attribute_path` is missing from %s", prefill_options, ) - return + logevent.object_ownership_check_improperly_configured( + submission, plugin=self + ) + raise ImproperlyConfigured( + f"`auth_attribute_path` missing from options {prefill_options}, cannot perform initial data ownership check" + ) with get_objects_client(api_group) as client: - validate_object_ownership(submission, client, auth_attribute_path) + validate_object_ownership(submission, client, auth_attribute_path, self) @classmethod def get_prefill_values_from_options( diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py index 5fed373f62..f57ffaa1af 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py @@ -89,11 +89,18 @@ def test_prefill_values_happy_flow(self): prefill_variables(submission=submission) state = submission.load_submission_value_variables_state() - self.assertEqual(TimelineLogProxy.objects.count(), 1) - logs = TimelineLogProxy.objects.get() + self.assertEqual(TimelineLogProxy.objects.count(), 2) + ownership_check_log, prefill_log = TimelineLogProxy.objects.all() - self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_success") - self.assertEqual(logs.extra_data["plugin_id"], "objects_api") + self.assertEqual( + ownership_check_log.extra_data["log_event"], + "object_ownership_check_success", + ) + self.assertEqual(ownership_check_log.extra_data["plugin_id"], "objects_api") + self.assertEqual( + prefill_log.extra_data["log_event"], "prefill_retrieve_success" + ) + self.assertEqual(prefill_log.extra_data["plugin_id"], "objects_api") self.assertEqual(state.variables["lastName"].value, "My last name") self.assertEqual(state.variables["age"].value, "45") @@ -183,10 +190,15 @@ def test_prefill_values_when_reference_returns_empty_values(self): prefill_variables(submission=submission) state = submission.load_submission_value_variables_state() - self.assertEqual(TimelineLogProxy.objects.count(), 1) - logs = TimelineLogProxy.objects.get() + self.assertEqual(TimelineLogProxy.objects.count(), 2) + ownership_check_log, prefill_log = TimelineLogProxy.objects.all() - self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_empty") - self.assertEqual(logs.extra_data["plugin_id"], "objects_api") + self.assertEqual( + ownership_check_log.extra_data["log_event"], + "object_ownership_check_success", + ) + self.assertEqual(ownership_check_log.extra_data["plugin_id"], "objects_api") + self.assertEqual(prefill_log.extra_data["log_event"], "prefill_retrieve_empty") + self.assertEqual(prefill_log.extra_data["plugin_id"], "objects_api") self.assertIsNone(state.variables["lastName"].value) self.assertIsNone(state.variables["age"].value) diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index 7a98ae136b..eef6438e2f 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -1,7 +1,7 @@ import logging from collections import defaultdict -from django.core.exceptions import PermissionDenied +from django.core.exceptions import ImproperlyConfigured, PermissionDenied import elasticapm from rest_framework.exceptions import ValidationError @@ -106,7 +106,7 @@ def fetch_prefill_values_from_options( plugin.verify_initial_data_ownership( submission, variable.form_variable.prefill_options ) - except PermissionDenied as exc: + except (PermissionDenied, ImproperlyConfigured) as exc: logevent.prefill_retrieve_failure(submission, plugin, exc) continue diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 9821892841..5531e71527 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -4,6 +4,7 @@ from functools import partial from typing import TYPE_CHECKING, Any, override +from django.core.exceptions import ImproperlyConfigured from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -15,6 +16,7 @@ ) from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig from openforms.contrib.objects_api.ownership_validation import validate_object_ownership +from openforms.logging import logevent from openforms.registrations.utils import execute_unless_result_exists from openforms.variables.service import get_static_variables @@ -188,11 +190,16 @@ def verify_initial_data_ownership(self, submission: Submission) -> None: auth_attribute_path = backend.options.get("auth_attribute_path") if not auth_attribute_path: - logger.info( + logger.error( "Cannot perform initial data ownership check, because backend %s has no `auth_attribute_path` configured", backend, ) - continue + logevent.object_ownership_check_improperly_configured( + submission, plugin=self + ) + raise ImproperlyConfigured( + f"{backend} has no `auth_attribute_path` configured, cannot perform initial data ownership check" + ) with get_objects_client(api_group) as client: - validate_object_ownership(submission, client, auth_attribute_path) + validate_object_ownership(submission, client, auth_attribute_path, self) diff --git a/src/openforms/registrations/tasks.py b/src/openforms/registrations/tasks.py index 373b381848..826beeb388 100644 --- a/src/openforms/registrations/tasks.py +++ b/src/openforms/registrations/tasks.py @@ -2,7 +2,6 @@ import traceback from contextlib import contextmanager -from django.core.exceptions import PermissionDenied from django.db import transaction from django.utils import timezone @@ -79,17 +78,7 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: # If an `initial_data_reference` was passed, we must verify that the # authenticated user is the owner of the referenced object if registration_plugin and submission.initial_data_reference: - try: - registration_plugin.verify_initial_data_ownership(submission) - except PermissionDenied as e: - logger.exception( - "Submission with initial_data_reference did not pass ownership check for plugin %s", - registration_plugin.verbose_name, - ) - logevent.object_ownership_check_failure( - submission, plugin=registration_plugin - ) - raise e + registration_plugin.verify_initial_data_ownership(submission) with transaction.atomic(): if not registration_plugin: From 12df2ff3894d62ee031c6973d96f72cbbd78370a Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 5 Nov 2024 15:21:53 +0100 Subject: [PATCH 13/39] :white_check_mark: [#4398] Update object ownership tests with log checks --- ...without_options_does_not_raise_error.yaml} | 6 +- ...ends_configured_does_not_raise_error.yaml} | 6 +- ...ests.test_user_is_not_owner_of_object.yaml | 6 +- ...owner_of_object_nested_auth_attribute.yaml | 14 +- ...torTests.test_user_is_owner_of_object.yaml | 6 +- .../setUpTestData.yaml | 8 +- .../tests/test_ownership_validation.py | 41 ++- ..._if_initial_data_reference_specified.yaml} | 6 +- .../setUpTestData.yaml | 8 +- ...nTests.test_prefill_values_happy_flow.yaml | 73 +++++- ...efill_values_when_reference_not_found.yaml | 50 +++- ...s_when_reference_returns_empty_values.yaml | 68 ++++- .../test_initial_data_ownership_validation.py | 235 ++++++++++-------- .../contrib/objects_api/tests/test_prefill.py | 7 +- .../test_initial_data_ownership_validation.py | 215 +++++++++------- 15 files changed, 488 insertions(+), 261 deletions(-) rename src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/{ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml => ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml} (70%) rename src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/{ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml => ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml} (70%) rename src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/{ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml => ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml} (70%) diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml similarity index 70% rename from src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml index ed0647d985..fe396c8123 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_raises_error.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 12:48:04 GMT + - Tue, 05 Nov 2024 14:28:43 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml similarity index 70% rename from src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml rename to src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml index ed0647d985..fe396c8123 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_raises_error.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 12:48:04 GMT + - Tue, 05 Nov 2024 14:28:43 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml index 332c63e9ec..9422e59933 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 29 Oct 2024 13:38:52 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml index 4f59403dbe..eee9ce3845 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"nested": {"bsn": "111222333"}, "foo": - "bar"}, "startAt": "2024-11-04"}}' + "bar"}, "startAt": "2024-11-05"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d","uuid":"9425a239-d592-4e02-b5dd-69609fdc292d","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95","uuid":"0fd71d89-3ec2-4593-8fb5-9e1822203c95","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 15:49:09 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Location: - - http://localhost:8002/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d + - http://localhost:8002/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95 Referrer-Policy: - same-origin Server: @@ -71,10 +71,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d + uri: http://localhost:8002/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/9425a239-d592-4e02-b5dd-69609fdc292d","uuid":"9425a239-d592-4e02-b5dd-69609fdc292d","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95","uuid":"0fd71d89-3ec2-4593-8fb5-9e1822203c95","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -89,7 +89,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 15:49:09 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml index 332c63e9ec..9422e59933 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 29 Oct 2024 13:38:52 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml index 4b65c9a456..0003a7470f 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"bsn": "111222333", "foo": "bar"}, "startAt": - "2024-10-29"}}' + "2024-11-05"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646","uuid":"d36124c0-752e-48a7-a724-bf3241a2e646","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-10-29","endAt":null,"registrationAt":"2024-10-29","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 29 Oct 2024 13:38:51 GMT + - Tue, 05 Nov 2024 14:28:43 GMT Location: - - http://localhost:8002/api/v2/objects/d36124c0-752e-48a7-a724-bf3241a2e646 + - http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 6cb8af0f7b..74e4cd2244 100644 --- a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -12,6 +12,8 @@ from openforms.contrib.objects_api.helpers import prepare_data_for_registration from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory from openforms.forms.tests.factories import FormRegistrationBackendFactory +from openforms.logging.models import TimelineLogProxy +from openforms.registrations.contrib.objects_api.plugin import ObjectsAPIRegistration from openforms.submissions.tests.factories import SubmissionFactory from openforms.utils.tests.vcr import OFVCRMixin @@ -20,6 +22,9 @@ TEST_FILES = (Path(__file__).parent / "files").resolve() +PLUGIN = ObjectsAPIRegistration("test") + + @override_settings( CORS_ALLOW_ALL_ORIGINS=False, ALLOWED_HOSTS=["*"], @@ -89,7 +94,7 @@ def test_user_is_owner_of_object(self): ) with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) @tag("gh-4398") def test_permission_denied_if_user_is_not_logged_in(self): @@ -97,11 +102,19 @@ def test_permission_denied_if_user_is_not_logged_in(self): with get_objects_client(self.objects_api_group_used) as client: with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) self.assertEqual( str(cm.exception), "Cannot pass data reference as anonymous user" ) + logs = TimelineLogProxy.objects.filter(object_id=submission.id) + self.assertEqual( + logs.filter( + extra_data__log_event="object_ownership_check_anonymous_user" + ).count(), + 1, + ) + @tag("gh-4398") def test_user_is_not_owner_of_object(self): submission = SubmissionFactory.create( @@ -124,11 +137,17 @@ def test_user_is_not_owner_of_object(self): with get_objects_client(self.objects_api_group_used) as client: with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) self.assertEqual( str(cm.exception), "User is not the owner of the referenced object" ) + logs = TimelineLogProxy.objects.filter(object_id=submission.id) + self.assertEqual( + logs.filter(extra_data__log_event="object_ownership_check_failure").count(), + 1, + ) + @tag("gh-4398") def test_user_is_not_owner_of_object_nested_auth_attribute(self): with get_objects_client(self.objects_api_group_used) as client: @@ -161,7 +180,7 @@ def test_user_is_not_owner_of_object_nested_auth_attribute(self): with get_objects_client(self.objects_api_group_used) as client: with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["nested", "bsn"]) + validate_object_ownership(submission, client, ["nested", "bsn"], PLUGIN) self.assertEqual( str(cm.exception), "User is not the owner of the referenced object" ) @@ -195,10 +214,10 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): ) with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) @tag("gh-4398") - def test_no_backends_configured_raises_error( + def test_no_backends_configured_does_not_raise_error( self, ): """ @@ -213,15 +232,15 @@ def test_no_backends_configured_raises_error( FormRegistrationBackendFactory.create(form=submission.form, backend="email") with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) @tag("gh-4398") - def test_backend_without_options_raises_error( + def test_backend_without_options_does_not_raise_error( self, ): """ - If the object could not be fetched due to misconfiguration, the ownership check - should not fail + If the object could not be fetched due to missing API group configuration, + the ownership check should not fail """ submission = SubmissionFactory.create( auth_info__value="111222333", @@ -234,4 +253,4 @@ def test_backend_without_options_raises_error( ) with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"]) + validate_object_ownership(submission, client, ["bsn"], PLUGIN) diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml similarity index 70% rename from src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml rename to src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml index 04ab29a804..b7527eec5a 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75 + uri: http://localhost:8002/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75","uuid":"351348b1-ff52-440f-8142-5e080b0a1b75","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff","uuid":"d23144bd-1220-42e6-9edf-d7fd31291bff","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 12:48:04 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml index 0406a729e6..cea8230065 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, - "startAt": "2024-11-04"}}' + "startAt": "2024-11-05"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75","uuid":"351348b1-ff52-440f-8142-5e080b0a1b75","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-04","endAt":null,"registrationAt":"2024-11-04","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff","uuid":"d23144bd-1220-42e6-9edf-d7fd31291bff","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Mon, 04 Nov 2024 12:48:04 GMT + - Tue, 05 Nov 2024 14:28:44 GMT Location: - - http://localhost:8002/api/v2/objects/351348b1-ff52-440f-8142-5e080b0a1b75 + - http://localhost:8002/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml index a2c6b47e2f..d69d558711 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", "record": {"typeVersion": 3, "data": {"name": {"last.name": "My last name"}, - "age": 45}, "startAt": "2024-11-26"}}' + "age": 45, "bsn": "111222333"}, "startAt": "2024-11-05"}}' headers: Accept: - '*/*' @@ -15,7 +15,7 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '210' + - '230' Content-Type: - application/json User-Agent: @@ -24,8 +24,8 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/78c78c8b-31b2-47a3-b894-5fca20674eed","uuid":"78c78c8b-31b2-47a3-b894-5fca20674eed","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My - last name"},"age":45},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My + last name"},"age":45,"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -34,15 +34,15 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '437' + - '455' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 11:03:35 GMT + - Tue, 05 Nov 2024 14:59:30 GMT Location: - - http://localhost:8002/api/v2/objects/78c78c8b-31b2-47a3-b894-5fca20674eed + - http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 Referrer-Policy: - same-origin Server: @@ -72,11 +72,11 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/78c78c8b-31b2-47a3-b894-5fca20674eed + uri: http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/78c78c8b-31b2-47a3-b894-5fca20674eed","uuid":"78c78c8b-31b2-47a3-b894-5fca20674eed","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"name":{"last.name":"My - last name"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -85,13 +85,62 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '437' + - '455' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 11:03:35 GMT + - Tue, 05 Nov 2024 14:59:31 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '455' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 05 Nov 2024 14:59:31 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml index 09f53f1ae0..d487947b3e 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml @@ -33,7 +33,55 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 11:03:35 GMT + - Tue, 05 Nov 2024 15:01:56 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 404 + message: Not Found +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/048a37ca-a602-4158-9e60-9f06f3e47e2a + response: + body: + string: '{"detail":"Not found."}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '23' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 05 Nov 2024 15:01:56 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml index a4c830d6c6..ff73d2e850 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml @@ -1,7 +1,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", - "record": {"typeVersion": 3, "data": {}, "startAt": "2024-11-26"}}' + "record": {"typeVersion": 3, "data": {"bsn": "111222333"}, "startAt": "2024-11-05"}}' headers: Accept: - '*/*' @@ -14,7 +14,7 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '162' + - '180' Content-Type: - application/json User-Agent: @@ -23,7 +23,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/cd89a036-4601-4c39-a53d-1f04eeb4f6de","uuid":"cd89a036-4601-4c39-a53d-1f04eeb4f6de","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -32,15 +32,15 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '393' + - '410' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 11:03:35 GMT + - Tue, 05 Nov 2024 14:59:25 GMT Location: - - http://localhost:8002/api/v2/objects/cd89a036-4601-4c39-a53d-1f04eeb4f6de + - http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 Referrer-Policy: - same-origin Server: @@ -70,10 +70,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/cd89a036-4601-4c39-a53d-1f04eeb4f6de + uri: http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/cd89a036-4601-4c39-a53d-1f04eeb4f6de","uuid":"cd89a036-4601-4c39-a53d-1f04eeb4f6de","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -82,13 +82,61 @@ interactions: Content-Crs: - EPSG:4326 Content-Length: - - '393' + - '410' Content-Type: - application/json Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 11:03:36 GMT + - Tue, 05 Nov 2024 14:59:25 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '410' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 05 Nov 2024 14:59:25 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py index bc1e68beba..0c6c852ef5 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -25,6 +25,7 @@ TEST_FILES = (Path(__file__).parent / "files").resolve() +@tag("gh-4398") class ObjectsAPIPrefillDataOwnershipCheckTests(OFVCRMixin, TestCase): VCR_TEST_FILES = TEST_FILES @@ -55,40 +56,38 @@ def setUpTestData(cls): ) cls.object_ref = object["uuid"] - @tag("gh-4398") - def test_verify_initial_data_ownership(self): - objects_api_group_used = ObjectsAPIGroupConfigFactory.create( + cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( for_test_docker_compose=True ) - objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() + cls.objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() - form = FormFactory.create() + cls.form = FormFactory.create() # An objects API backend with a different API group FormRegistrationBackendFactory.create( - form=form, + form=cls.form, backend="objects_api", options={ "version": 2, "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_unused.pk, + "objects_api_group": cls.objects_api_group_unused.pk, "objecttype_version": 1, }, ) # Another backend that should be ignored - FormRegistrationBackendFactory.create(form=form, backend="email") + FormRegistrationBackendFactory.create(form=cls.form, backend="email") # The backend that should be used to perform the check FormRegistrationBackendFactory.create( - form=form, + form=cls.form, backend="objects_api", options={ "version": 2, "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_used.pk, + "objects_api_group": cls.objects_api_group_used.pk, "objecttype_version": 1, }, ) - form_step = FormStepFactory.create( + cls.form_step = FormStepFactory.create( form_definition__configuration={ "components": [ { @@ -99,15 +98,15 @@ def test_verify_initial_data_ownership(self): ] } ) - variable = FormVariableFactory.create( + cls.variable = FormVariableFactory.create( key="voornamen", - form=form_step.form, + form=cls.form_step.form, prefill_plugin="objects_api", prefill_attribute="", prefill_options={ "version": 2, "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_used.pk, + "objects_api_group": cls.objects_api_group_used.pk, "objecttype_version": 1, "auth_attribute_path": ["nested", "bsn"], "variables_mapping": [ @@ -116,112 +115,138 @@ def test_verify_initial_data_ownership(self): }, ) - with self.subTest( - "verify_initial_data_ownership is called if initial_data_reference is specified" - ): - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, - ) - - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership" - ) as mock_validate_object_ownership: - prefill_variables(submission=submission_step.submission) - - self.assertEqual(mock_validate_object_ownership.call_count, 1) + def test_verify_initial_data_ownership_called_if_initial_data_reference_specified( + self, + ): + submission_step = SubmissionStepFactory.create( + submission__form=self.form_step.form, + form_step=self.form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership" + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) - self.assertEqual(call.args[0], submission_step.submission) - self.assertEqual( - call.args[1].base_url, - objects_api_group_used.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["nested", "bsn"]) + self.assertEqual(mock_validate_object_ownership.call_count, 1) - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + self.assertEqual(call.args[0], submission_step.submission) self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 + call.args[1].base_url, + self.objects_api_group_used.objects_service.api_root, ) + self.assertEqual(call.args[2], ["nested", "bsn"]) - with self.subTest( - "verify_initial_data_ownership raising error causes prefill to fail" - ): - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, - ) + logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: - prefill_variables(submission=submission_step.submission) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 + ) - self.assertEqual(mock_validate_object_ownership.call_count, 1) + def test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail(self): + submission_step = SubmissionStepFactory.create( + submission__form=self.form_step.form, + form_step=self.form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) - self.assertEqual(call.args[0], submission_step.submission) - self.assertEqual( - call.args[1].base_url, - objects_api_group_used.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["nested", "bsn"]) + self.assertEqual(mock_validate_object_ownership.call_count, 1) - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + + self.assertEqual(call.args[0], submission_step.submission) self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + call.args[1].base_url, + self.objects_api_group_used.objects_service.api_root, ) + self.assertEqual(call.args[2], ["nested", "bsn"]) - with self.subTest( - "verify_initial_data_ownership does not raise errors if no API group is found" - ): - variable.prefill_options["objects_api_group"] = ( - ObjectsAPIGroupConfig.objects.last().pk + 1 - ) - variable.save() - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, - ) + logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", - ) as mock_validate_object_ownership: - prefill_variables(submission=submission_step.submission) + def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failing_prefill( + self, + ): + del self.variable.prefill_options["auth_attribute_path"] + self.variable.save() + submission_step = SubmissionStepFactory.create( + submission__form=self.form_step.form, + form_step=self.form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) - self.assertEqual(mock_validate_object_ownership.call_count, 0) + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) - # Prefilling fails, because the API group does not exist - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 - ) + self.assertEqual(mock_validate_object_ownership.call_count, 0) + + logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) + self.assertEqual( + logs.filter( + extra_data__log_event="object_ownership_check_improperly_configured" + ).count(), + 1, + ) + + def test_verify_initial_data_ownership_does_not_raise_errors_without_api_group( + self, + ): + self.variable.prefill_options["objects_api_group"] = ( + ObjectsAPIGroupConfig.objects.last().pk + 1 + ) + self.variable.save() + submission_step = SubmissionStepFactory.create( + submission__form=self.form_step.form, + form_step=self.form_step, + submission__auth_info__value="999990676", + submission__auth_info__attribute=AuthAttribute.bsn, + submission__initial_data_reference=self.object_ref, + ) + + with patch( + "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", + ) as mock_validate_object_ownership: + prefill_variables(submission=submission_step.submission) + + self.assertEqual(mock_validate_object_ownership.call_count, 0) + + logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 + ) + # Prefilling fails, because the API group does not exist + self.assertEqual( + logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 + ) diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py index f57ffaa1af..7ecaa8e3c8 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py @@ -83,6 +83,7 @@ def test_prefill_values_happy_flow(self): {"variable_key": "lastName", "target_path": ["name", "last.name"]}, {"variable_key": "age", "target_path": ["age"]}, ], + "auth_attribute_path": ["bsn"], }, ) @@ -106,6 +107,8 @@ def test_prefill_values_happy_flow(self): def test_prefill_values_when_reference_not_found(self): submission = SubmissionFactory.from_components( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, initial_data_reference="048a37ca-a602-4158-9e60-9f06f3e47e2a", components_list=[ { @@ -131,6 +134,7 @@ def test_prefill_values_when_reference_not_found(self): {"variable_key": "lastName", "target_path": ["name", "last.name"]}, {"variable_key": "age", "target_path": ["age"]}, ], + "auth_attribute_path": ["bsn"], }, ) @@ -150,7 +154,7 @@ def test_prefill_values_when_reference_returns_empty_values(self): with get_objects_client(self.objects_api_group) as client: created_obj = client.create_object( record_data=prepare_data_for_registration( - data={}, + data={"bsn": "111222333"}, objecttype_version=3, ), objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", @@ -184,6 +188,7 @@ def test_prefill_values_when_reference_returns_empty_values(self): {"variable_key": "lastName", "target_path": ["name", "last.name"]}, {"variable_key": "age", "target_path": ["age"]}, ], + "auth_attribute_path": ["bsn"], }, ) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 3603959c20..51e72721d2 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -1,136 +1,169 @@ from unittest.mock import patch -from django.core.exceptions import PermissionDenied +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.test import TestCase, tag from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory from openforms.forms.tests.factories import FormFactory, FormRegistrationBackendFactory +from openforms.logging.models import TimelineLogProxy from openforms.submissions.constants import PostSubmissionEvents from openforms.submissions.tasks.registration import pre_registration from openforms.submissions.tests.factories import SubmissionFactory -class ObjectsAPIPreRegistrationTests(TestCase): - @tag("gh-4398") - def test_verify_initial_data_ownership(self): - objects_api_group_used = ObjectsAPIGroupConfigFactory.create( +@tag("gh-4398") +class ObjectsAPIPrefillDataOwnershipCheckTests(TestCase): + def setUp(self): + super().setUp() + + self.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( for_test_docker_compose=True ) - objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() + self.objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() + + self.form = FormFactory.create() - form = FormFactory.create() - # An objects API backend that is missing `auth_attribute_path` - FormRegistrationBackendFactory.create( - form=form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_unused.pk, - "objecttype_version": 1, - }, - ) # An objects API backend with a different API group FormRegistrationBackendFactory.create( - form=form, + form=self.form, backend="objects_api", options={ "version": 2, "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_unused.pk, + "objects_api_group": self.objects_api_group_unused.pk, "objecttype_version": 1, "auth_attribute_path": ["bsn"], }, ) # Another backend that should be ignored - FormRegistrationBackendFactory.create(form=form, backend="email") + FormRegistrationBackendFactory.create(form=self.form, backend="email") # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=form, + self.backend = FormRegistrationBackendFactory.create( + form=self.form, backend="objects_api", options={ "version": 2, "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": objects_api_group_used.pk, + "objects_api_group": self.objects_api_group_used.pk, "objecttype_version": 1, "auth_attribute_path": ["nested", "bsn"], }, ) - with self.subTest( - "verify_initial_data_ownership is not called if no initial_data_reference is specified" - ): - submission = SubmissionFactory.create( - form=form, - completed_not_preregistered=True, + def test_verify_initial_data_ownership_not_called_if_initial_data_reference_missing( + self, + ): + submission = SubmissionFactory.create( + form=self.form, + completed_not_preregistered=True, + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + mock_validate_object_ownership.assert_not_called() + + def test_verify_initial_data_ownership_called_if_initial_data_reference_specified( + self, + ): + submission = SubmissionFactory.create( + form=self.form, + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership" + ) as mock_validate_object_ownership: + pre_registration(submission.id, PostSubmissionEvents.on_completion) + + self.assertEqual(mock_validate_object_ownership.call_count, 2) + + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call1, call2 = mock_validate_object_ownership.mock_calls + + self.assertEqual(call1.args[0], submission) + self.assertEqual( + call1.args[1].base_url, + self.objects_api_group_unused.objects_service.api_root, ) + self.assertEqual(call1.args[2], ["bsn"]) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: + self.assertEqual(call2.args[0], submission) + self.assertEqual( + call2.args[1].base_url, + self.objects_api_group_used.objects_service.api_root, + ) + self.assertEqual(call2.args[2], ["nested", "bsn"]) + + def test_verify_initial_data_ownership_raising_error_causes_failing_pre_registration( + self, + ): + submission = SubmissionFactory.create( + form=self.form, + completed_not_preregistered=True, + initial_data_reference="1234", + ) + + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", + side_effect=PermissionDenied, + ) as mock_validate_object_ownership: + with self.assertRaises(PermissionDenied): pre_registration(submission.id, PostSubmissionEvents.on_completion) + self.assertEqual(mock_validate_object_ownership.call_count, 1) - mock_validate_object_ownership.assert_not_called() + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] - with self.subTest( - "verify_initial_data_ownership is called if initial_data_reference exists is specified" - ): - submission = SubmissionFactory.create( - form=form, - completed_not_preregistered=True, - initial_data_reference="1234", + self.assertEqual(call.args[0], submission) + self.assertEqual( + call.args[1].base_url, + self.objects_api_group_unused.objects_service.api_root, ) + self.assertEqual(call.args[2], ["bsn"]) + + def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failing_pre_registration( + self, + ): + del self.backend.options["auth_attribute_path"] + self.backend.save() + + submission = SubmissionFactory.create( + form=self.form, + completed_not_preregistered=True, + initial_data_reference="1234", + ) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership" - ) as mock_validate_object_ownership: + with patch( + "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", + ) as mock_validate_object_ownership: + with self.assertRaises(ImproperlyConfigured): pre_registration(submission.id, PostSubmissionEvents.on_completion) - self.assertEqual(mock_validate_object_ownership.call_count, 2) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call1, call2 = mock_validate_object_ownership.mock_calls - - self.assertEqual(call1.args[0], submission) - self.assertEqual( - call1.args[1].base_url, - objects_api_group_unused.objects_service.api_root, - ) - self.assertEqual(call1.args[2], ["bsn"]) - - self.assertEqual(call2.args[0], submission) - self.assertEqual( - call2.args[1].base_url, - objects_api_group_used.objects_service.api_root, - ) - self.assertEqual(call2.args[2], ["nested", "bsn"]) - - with self.subTest( - "verify_initial_data_ownership raising error causes pre registration to fail" - ): - submission = SubmissionFactory.create( - form=form, - completed_not_preregistered=True, - initial_data_reference="1234", - ) + # Called once before crashing due to missing `auth_attribute_path` + self.assertEqual(mock_validate_object_ownership.call_count, 1) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: - with self.assertRaises(PermissionDenied): - pre_registration(submission.id, PostSubmissionEvents.on_completion) - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission) - self.assertEqual( - call.args[1].base_url, - objects_api_group_unused.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["bsn"]) + # Cannot compare with `.assert_has_calls`, because the client objects + # won't match + call = mock_validate_object_ownership.mock_calls[0] + + self.assertEqual(call.args[0], submission) + self.assertEqual( + call.args[1].base_url, + self.objects_api_group_unused.objects_service.api_root, + ) + self.assertEqual(call.args[2], ["bsn"]) + + logs = TimelineLogProxy.objects.filter(object_id=submission.id) + self.assertEqual( + logs.filter( + extra_data__log_event="object_ownership_check_improperly_configured" + ).count(), + 1, + ) From 88d14da7cc92fda44c8eee39792c72262be589b7 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 12 Nov 2024 12:51:57 +0100 Subject: [PATCH 14/39] :recycle: [#4398] Use submission.registration_backend in registration ownership check previously this looped over possible backends, but at the time of executing the `verify_initial_data_ownership` check, the registration_backend that is to be used is already known --- .../contrib/objects_api/plugin.py | 49 +++++++++---------- .../test_initial_data_ownership_validation.py | 34 ++++--------- 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 5531e71527..39cd8640ee 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -176,30 +176,27 @@ def get_variables(self) -> list[FormVariable]: return get_static_variables(variables_registry=variables_registry) def verify_initial_data_ownership(self, submission: Submission) -> None: - for backend in submission.form.registration_backends.filter( - backend=self.identifier - ): - if not backend.options: - continue - - api_group = ObjectsAPIGroupConfig.objects.filter( - pk=backend.options.get("objects_api_group") - ).first() - if not api_group: - continue - - auth_attribute_path = backend.options.get("auth_attribute_path") - if not auth_attribute_path: - logger.error( - "Cannot perform initial data ownership check, because backend %s has no `auth_attribute_path` configured", - backend, - ) - logevent.object_ownership_check_improperly_configured( - submission, plugin=self - ) - raise ImproperlyConfigured( - f"{backend} has no `auth_attribute_path` configured, cannot perform initial data ownership check" - ) + assert submission.registration_backend + backend = submission.registration_backend + + api_group = ObjectsAPIGroupConfig.objects.filter( + pk=backend.options.get("objects_api_group") + ).first() + if not api_group: + return + + auth_attribute_path = backend.options.get("auth_attribute_path") + if not auth_attribute_path: + logger.error( + "Cannot perform initial data ownership check, because backend %s has no `auth_attribute_path` configured", + backend, + ) + logevent.object_ownership_check_improperly_configured( + submission, plugin=self + ) + raise ImproperlyConfigured( + f"{backend} has no `auth_attribute_path` configured, cannot perform initial data ownership check" + ) - with get_objects_client(api_group) as client: - validate_object_ownership(submission, client, auth_attribute_path, self) + with get_objects_client(api_group) as client: + validate_object_ownership(submission, client, auth_attribute_path, self) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 51e72721d2..bee9c1e967 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -73,6 +73,7 @@ def test_verify_initial_data_ownership_called_if_initial_data_reference_specifie form=self.form, completed_not_preregistered=True, initial_data_reference="1234", + finalised_registration_backend_key=self.backend.key, ) with patch( @@ -80,25 +81,18 @@ def test_verify_initial_data_ownership_called_if_initial_data_reference_specifie ) as mock_validate_object_ownership: pre_registration(submission.id, PostSubmissionEvents.on_completion) - self.assertEqual(mock_validate_object_ownership.call_count, 2) + self.assertEqual(mock_validate_object_ownership.call_count, 1) # Cannot compare with `.assert_has_calls`, because the client objects # won't match - call1, call2 = mock_validate_object_ownership.mock_calls - - self.assertEqual(call1.args[0], submission) - self.assertEqual( - call1.args[1].base_url, - self.objects_api_group_unused.objects_service.api_root, - ) - self.assertEqual(call1.args[2], ["bsn"]) + call = mock_validate_object_ownership.mock_calls[0] - self.assertEqual(call2.args[0], submission) + self.assertEqual(call.args[0], submission) self.assertEqual( - call2.args[1].base_url, + call.args[1].base_url, self.objects_api_group_used.objects_service.api_root, ) - self.assertEqual(call2.args[2], ["nested", "bsn"]) + self.assertEqual(call.args[2], ["nested", "bsn"]) def test_verify_initial_data_ownership_raising_error_causes_failing_pre_registration( self, @@ -138,6 +132,7 @@ def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failin form=self.form, completed_not_preregistered=True, initial_data_reference="1234", + finalised_registration_backend_key=self.backend.key, ) with patch( @@ -146,19 +141,8 @@ def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failin with self.assertRaises(ImproperlyConfigured): pre_registration(submission.id, PostSubmissionEvents.on_completion) - # Called once before crashing due to missing `auth_attribute_path` - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission) - self.assertEqual( - call.args[1].base_url, - self.objects_api_group_unused.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["bsn"]) + # Not called, due to missing `auth_attribute_path` + self.assertEqual(mock_validate_object_ownership.call_count, 0) logs = TimelineLogProxy.objects.filter(object_id=submission.id) self.assertEqual( From 116fa15181993dc04749d929127e821a3e9a7e93 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 12 Nov 2024 14:03:06 +0100 Subject: [PATCH 15/39] :sparkles: [#4398] Make auth_attribute_path required if update existing object True this field is necessary to perform the initial data reference ownership check, which is performed when updating existing objects --- .../form_design/RegistrationFields.stories.js | 31 ++++++++++++++++++- .../objectsapi/fields/AuthAttributePath.js | 3 +- .../contrib/objects_api/config.py | 10 ++++++ .../objects_api/tests/test_serializer.py | 17 ++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js index 54035a01e9..4bd397d8d0 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -15,6 +15,7 @@ import { mockRoleTypesGet as mockZGWApisRoleTypesGet, } from 'components/admin/form_design/registrations/zgw/mocks'; import { + FeatureFlagsDecorator, FormDecorator, ValidationErrorsDecorator, } from 'components/admin/form_design/story-decorators'; @@ -24,7 +25,7 @@ import RegistrationFields from './RegistrationFields'; export default { title: 'Form design / Registration / RegistrationFields', - decorators: [ValidationErrorsDecorator, FormDecorator], + decorators: [FeatureFlagsDecorator, ValidationErrorsDecorator, FormDecorator], component: RegistrationFields, args: { availableBackends: [ @@ -689,6 +690,11 @@ export const ConfiguredBackends = { }; export const ObjectsAPI = { + parameters: { + featureFlags: { + REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION: true, + }, + }, args: { configuredBackends: [ { @@ -740,6 +746,29 @@ export const ObjectsAPI = { await rsSelect(catalogueSelect, 'Catalogus 2'); }); + await step( + 'Path to auth attribute is required if updating existing objects is enabled', + async () => { + const otherSettingsTitle = modal.getByRole('heading', { + name: 'Overige instellingen (Tonen)', + }); + expect(otherSettingsTitle).toBeVisible(); + await userEvent.click(within(otherSettingsTitle).getByRole('link', {name: '(Tonen)'})); + + const authAttributePath = modal.getByText( + 'Path to auth attribute (e.g. BSN/KVK) in objects' + ); + + expect(authAttributePath).not.toHaveClass('required'); + + const updateExistingObject = modal.getByLabelText('Bestaand object bijwerken'); + await userEvent.click(updateExistingObject); + + // Checking `updateExistingObject` should make `authAttributePath` required + expect(authAttributePath).toHaveClass('required'); + } + ); + await step('Submit the form', async () => { await userEvent.click(modal.getByRole('button', {name: 'Opslaan'})); expect(args.onChange).toHaveBeenCalled(); diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js index 55c2e1e523..a7a323cb8b 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js @@ -6,10 +6,10 @@ import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import ArrayInput from 'components/admin/forms/ArrayInput'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; -import {Checkbox} from 'components/admin/forms/Inputs'; const AuthAttributePath = () => { const [fieldProps, , fieldHelpers] = useField({name: 'authAttributePath', type: 'array'}); + const [updateExistingObject] = useField('updateExistingObject'); const {setValue} = fieldHelpers; const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = useContext(FeatureFlagsContext); @@ -33,6 +33,7 @@ const AuthAttributePath = () => { defaultMessage="This is used to perform validation to verify that the authenticated user is the owner of the object." /> } + required={!!updateExistingObject.value} > RegistrationOptions: {"version": _("Unknown version: {version}").format(version=version)} ) + if attrs.get("update_existing_object") and not attrs.get("auth_attribute_path"): + raise serializers.ValidationError( + { + "auth_attribute_path": _( + 'This field is required if "Update existing object" is checked' + ) + }, + code="required", + ) + if not self.context.get("validate_business_logic", True): return attrs diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py b/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py index 59dc1e7332..de84b30108 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py @@ -188,6 +188,23 @@ def test_invalid_objects_api_group(self): error = options.errors["objects_api_group"][0] self.assertEqual(error.code, "does_not_exist") + def test_auth_attribute_path_required_if_update_existing_object_is_true(self): + options = ObjectsAPIOptionsSerializer( + data={ + "objects_api_group": self.objects_api_group.pk, + "version": 2, + "objecttype": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 1, + "update_existing_object": True, + "auth_attribute_path": [], + }, + ) + + self.assertFalse(options.is_valid()) + self.assertIn("auth_attribute_path", options.errors) + error = options.errors["auth_attribute_path"][0] + self.assertEqual(error.code, "required") + def test_valid_serializer(self): options = ObjectsAPIOptionsSerializer( data={ From 3916207abc6a2490ead5c3edeaedaccb3eff6a43 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 25 Nov 2024 12:01:18 +0100 Subject: [PATCH 16/39] :whale: Update objecttypes docker fixture * update readme to use the correct command * add `bsn` field to Person objecttype to use it for Objects API prefill ownership check --- docker/objects-apis/README.md | 1 + .../fixtures/objecttypes_api_fixtures.json | 73 ++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docker/objects-apis/README.md b/docker/objects-apis/README.md index 388ada6fe6..b838abba7d 100644 --- a/docker/objects-apis/README.md +++ b/docker/objects-apis/README.md @@ -38,6 +38,7 @@ docker compose -f docker-compose.objects-apis.yml run objecttypes-web \ --indent=4 \ --output /app/fixtures/objecttypes_api_fixtures.json \ core.objecttype \ + core.objectversion \ token ``` diff --git a/docker/objects-apis/fixtures/objecttypes_api_fixtures.json b/docker/objects-apis/fixtures/objecttypes_api_fixtures.json index f4cd1edf02..a4c8ebd262 100644 --- a/docker/objects-apis/fixtures/objecttypes_api_fixtures.json +++ b/docker/objects-apis/fixtures/objecttypes_api_fixtures.json @@ -18,7 +18,7 @@ "documentation_url": "", "labels": {}, "created_at": "2023-10-24", - "modified_at": "2024-02-08", + "modified_at": "2024-11-25", "allow_geometry": true } }, @@ -210,8 +210,8 @@ "object_type": 1, "version": 3, "created_at": "2024-02-08", - "modified_at": "2024-02-08", - "published_at": "2024-02-08", + "modified_at": "2024-11-25", + "published_at": "2024-11-25", "json_schema": { "$id": "https://example.com/person.schema.json", "type": "object", @@ -262,7 +262,7 @@ } } }, - "status": "draft" + "status": "published" } }, { @@ -389,6 +389,71 @@ "status": "draft" } }, +{ + "model": "core.objectversion", + "pk": 9, + "fields": { + "object_type": 1, + "version": 4, + "created_at": "2024-11-25", + "modified_at": "2024-11-25", + "published_at": "2024-11-25", + "json_schema": { + "$id": "https://example.com/person.schema.json", + "type": "object", + "title": "Person", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "age": { + "type": "integer", + "minimum": 18 + }, + "bsn": { + "type": "string" + }, + "name": { + "type": "object", + "properties": { + "last.name": { + "type": "string" + } + } + }, + "nested": { + "type": "object", + "properties": { + "unrelated": { + "type": "string" + }, + "submission_payment_amount": { + "type": "number", + "multipleOf": 0.01 + } + } + }, + "submission_date": { + "type": "string", + "format": "date-time" + }, + "submission_csv_url": { + "type": "string", + "format": "uri" + }, + "submission_pdf_url": { + "type": "string", + "format": "uri" + }, + "submission_payment_completed": { + "type": "boolean" + }, + "submission_payment_public_ids": { + "type": "array" + } + } + }, + "status": "draft" + } +}, { "model": "token.tokenauth", "pk": 1, From 3e1f8341255336615757d62a5b20309c7b9a9ab1 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 25 Nov 2024 12:06:55 +0100 Subject: [PATCH 17/39] :sparkles: [#4398] Add authAttributePath to objects prefill form --- .../forms/tests/variables/test_viewset.py | 2 +- .../objectsapi/LegacyConfigFields.js | 161 +++++++++--------- .../objectsapi/ObjectsApiOptionsForm.js | 2 +- .../objectsapi/V2ConfigFields.js | 48 +++--- .../registrations/objectsapi/fields/index.js | 1 - .../variables/VariablesEditor.stories.js | 4 + ...opyConfigurationFromRegistrationBackend.js | 3 +- .../prefill/objects_api/ObjectsAPIFields.js | 49 +++--- .../js/components/admin/forms/ArrayInput.js | 8 +- .../components/admin/forms/VariableMapping.js | 2 +- .../objects_api}/AuthAttributePath.js | 25 ++- .../admin/forms/objects_api/index.js | 1 + 12 files changed, 167 insertions(+), 139 deletions(-) rename src/openforms/js/components/admin/{form_design/registrations/objectsapi/fields => forms/objects_api}/AuthAttributePath.js (62%) diff --git a/src/openforms/forms/tests/variables/test_viewset.py b/src/openforms/forms/tests/variables/test_viewset.py index 8796f70483..6d06bb57d7 100644 --- a/src/openforms/forms/tests/variables/test_viewset.py +++ b/src/openforms/forms/tests/variables/test_viewset.py @@ -1035,7 +1035,7 @@ def test_bulk_create_and_update_with_prefill_constraints(self): "source": FormVariableSources.user_defined, "prefill_plugin": "objects_api", "prefill_attribute": "", - "prefill_options": {"foo": "bar"}, + "prefill_options": {"foo": "bar", "auth_attribute_path": ["bsn"]}, } ] diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js index 4e4f8a18dd..ba80b822f9 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js @@ -1,12 +1,15 @@ import {useField} from 'formik'; import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; +import {useContext} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; +import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {TextArea, TextInput} from 'components/admin/forms/Inputs'; import { + AuthAttributePath, ObjectTypeSelect, ObjectTypeVersionSelect, ObjectsAPIGroup, @@ -14,7 +17,6 @@ import { import ErrorBoundary from 'components/errors/ErrorBoundary'; import { - AuthAttributePath, DocumentTypesFieldet, LegacyDocumentTypesFieldet, OrganisationRSIN, @@ -31,89 +33,94 @@ const onApiGroupChange = prevValues => ({ objecttypeVersion: undefined, }); -const LegacyConfigFields = ({apiGroupChoices}) => ( - <> -
- +const LegacyConfigFields = ({apiGroupChoices}) => { + const intl = useIntl(); + const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = + useContext(FeatureFlagsContext); + + const [updateExistingObject] = useField('updateExistingObject'); + const authAttributePathRequired = !!updateExistingObject.value; + + return ( + <> +
+ + + } + > + + + +
+ +
+ } + collapsible + initialCollapsed={false} + > + + + +
+ } > - - } - helpText={ - - } - /> - - } - /> + -
-
- } - collapsible - initialCollapsed={false} - > - - - -
- - - } - > - - - - - -
- } - collapsible - fieldNames={['organisatieRsin']} - > - - - - -
- -); + + +
+ } + collapsible + fieldNames={['organisatieRsin']} + > + + + {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION ? ( + + ) : null} + +
+ + ); +}; LegacyConfigFields.propTypes = { apiGroupChoices: PropTypes.arrayOf( diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js index b84e8bcff8..586518e70c 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js @@ -56,7 +56,7 @@ ObjectsApiOptionsForm.propTypes = { objecttype: PropTypes.string, objecttypeVersion: PropTypes.number, updateExistingObject: PropTypes.bool, - authAttributePath: PropTypes.bool, + authAttributePath: PropTypes.array, productaanvraagType: PropTypes.string, informatieobjecttypeSubmissionReport: PropTypes.string, uploadSubmissionCsv: PropTypes.bool, diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js index 2b6efed72b..b5f67d4ecd 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js @@ -1,10 +1,14 @@ -import {useFormikContext} from 'formik'; +import {useField, useFormikContext} from 'formik'; import PropTypes from 'prop-types'; +import {useContext} from 'react'; +import {useIntl} from 'react-intl'; import {FormattedMessage} from 'react-intl'; +import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import useConfirm from 'components/admin/form_design/useConfirm'; import Fieldset from 'components/admin/forms/Fieldset'; import { + AuthAttributePath, ObjectTypeSelect, ObjectTypeVersionSelect, ObjectsAPIGroup, @@ -12,7 +16,6 @@ import { import ErrorBoundary from 'components/errors/ErrorBoundary'; import { - AuthAttributePath, DocumentTypesFieldet, LegacyDocumentTypesFieldet, OrganisationRSIN, @@ -31,6 +34,13 @@ const onApiGroupChange = prevValues => ({ }); const V2ConfigFields = ({apiGroupChoices}) => { + const intl = useIntl(); + const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = + useContext(FeatureFlagsContext); + + const [updateExistingObject] = useField('updateExistingObject'); + const authAttributePathRequired = !!updateExistingObject.value; + const { values: {variablesMapping = []}, setFieldValue, @@ -70,18 +80,14 @@ const V2ConfigFields = ({apiGroupChoices}) => { } > - } - helpText={ - - } + label={intl.formatMessage({ + description: "Objects API registration options 'Objecttype' label", + defaultMessage: 'Objecttype', + })} + helpText={intl.formatMessage({ + description: "Objects API registration options 'Objecttype' helpText", + defaultMessage: 'The registration result will be an object from the selected type.', + })} onChangeCheck={async () => { if (variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmation(); @@ -101,12 +107,10 @@ const V2ConfigFields = ({apiGroupChoices}) => { } > - } + label={intl.formatMessage({ + description: "Objects API registration options 'objecttypeVersion' label", + defaultMessage: 'Version', + })} /> @@ -136,7 +140,9 @@ const V2ConfigFields = ({apiGroupChoices}) => { > - + {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION ? ( + + ) : null} diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js index eb718ea817..bc81b78752 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/index.js @@ -3,4 +3,3 @@ export {DocumentTypesFieldet} from './DocumentTypes'; export {default as UpdateExistingObject} from './UpdateExistingObject'; export {default as UploadSubmissionCsv} from './UploadSubmissionCSV'; export {default as OrganisationRSIN} from './OrganisationRSIN'; -export {default as AuthAttributePath} from './AuthAttributePath'; diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index ed1c30a76d..b2802c1656 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -639,6 +639,7 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { objectsApiGroup: 1, objecttype: '2c77babf-a967-4057-9969-0200320d23f1', objecttypeVersion: 2, + authAttributePath: ['path', 'to', 'bsn'], variablesMapping: [ { variableKey: 'formioComponent', @@ -730,6 +731,9 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { 'options.objecttypeUuid': '2c77babf-a967-4057-9969-0200320d23f1', 'options.objecttypeVersion': '2', }); + expect(canvas.getByTestId('options.authAttributePath-0')).toHaveValue('path'); + expect(canvas.getByTestId('options.authAttributePath-1')).toHaveValue('to'); + expect(canvas.getByTestId('options.authAttributePath-2')).toHaveValue('bsn'); expect(propertyDropdowns[0]).toHaveValue(serializeValue(['height'])); expect(propertyDropdowns[1]).toHaveValue(serializeValue(['species'])); diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js index 1292b6aba4..b4d4d44c25 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js @@ -58,7 +58,7 @@ const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) onClick={async e => { e.preventDefault(); const confirmSwitch = await openCopyConfigurationConfirmationModal(); - if (confirmSwitch) { + if (confirmSwitch && selectedBackend) { setValues(prevValues => ({ ...prevValues, // Trying to set multiple nested values doesn't work, since it sets them @@ -68,6 +68,7 @@ const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) objectsApiGroup: selectedBackend.options.objectsApiGroup, objecttypeUuid: selectedBackend.options.objecttype, objecttypeVersion: selectedBackend.options.objecttypeVersion, + authAttributePath: selectedBackend.options.authAttributePath, variablesMapping: selectedBackend.options.variablesMapping, }, })); diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index 5f7b0d375f..b077f4490a 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -5,7 +5,7 @@ */ import {useFormikContext} from 'formik'; import PropTypes from 'prop-types'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import useAsync from 'react-use/esm/useAsync'; @@ -16,6 +16,7 @@ import FormRow from 'components/admin/forms/FormRow'; import {LOADING_OPTION} from 'components/admin/forms/Select'; import VariableMapping from 'components/admin/forms/VariableMapping'; import { + AuthAttributePath, ObjectTypeSelect, ObjectTypeVersionSelect, ObjectsAPIGroup, @@ -38,6 +39,7 @@ const onApiGroupChange = prevValues => ({ ...prevValues.options, objecttypeUuid: '', objecttypeVersion: undefined, + authAttributePath: [], variablesMapping: [], }, }); @@ -60,7 +62,13 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { values, values: { plugin, - options: {objecttypeUuid, objecttypeVersion, objectsApiGroup, variablesMapping}, + options: { + objecttypeUuid, + objecttypeVersion, + objectsApiGroup, + authAttributePath, + variablesMapping, + }, }, setFieldValue, } = useFormikContext(); @@ -69,6 +77,7 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { objectsApiGroup: '', objecttypeUuid: '', objecttypeVersion: null, + authAttributePath: [], variablesMapping: [], }; @@ -120,7 +129,7 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { return ( <> - {showCopyButton && backends.length ? ( + {showCopyButton ? ( { if (!objecttypeUuid) return true; const confirmSwitch = await openApiGroupConfirmationModal(); if (!confirmSwitch) return false; + setFieldValue('options.authAttributePath', []); setFieldValue('options.variablesMapping', []); return true; }} @@ -154,22 +164,20 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { name="options.objecttypeUuid" apiGroupFieldName="options.objectsApiGroup" versionFieldName="options.objecttypeVersion" - label={ - - } - helpText={ - - } + label={intl.formatMessage({ + description: "Objects API prefill options 'Objecttype' label", + defaultMessage: 'Objecttype', + })} + helpText={intl.formatMessage({ + description: "Objects API prefill options 'Objecttype' helpText", + defaultMessage: + 'The prefill values will be taken from an object of the selected type.', + })} onChangeCheck={async () => { if (values.options.variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmationModal(); if (!confirmSwitch) return false; + setFieldValue('options.authAttributePath', []); setFieldValue('options.variablesMapping', []); return true; }} @@ -188,16 +196,15 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { > - } + label={intl.formatMessage({ + description: "Objects API prefill options 'objecttypeVersion' label", + defaultMessage: 'Version', + })} apiGroupFieldName="options.objectsApiGroup" objectTypeFieldName="options.objecttypeUuid" /> +
{ + setInputs([...values]); + }, [values]); + const onAdd = event => { setInputs(inputs.concat([''])); }; @@ -65,6 +70,7 @@ const ArrayInput = ({ type={inputType} value={value} onChange={onInputChange.bind(null, index)} + data-testid={`${name}-${index}`} {...extraProps} /> diff --git a/src/openforms/js/components/admin/forms/VariableMapping.js b/src/openforms/js/components/admin/forms/VariableMapping.js index 825abd82d5..bab5846fe1 100644 --- a/src/openforms/js/components/admin/forms/VariableMapping.js +++ b/src/openforms/js/components/admin/forms/VariableMapping.js @@ -255,7 +255,7 @@ const VariableMapping = ({ // TODO update const initial = {[variableName]: '', [propertyName]: ''}; const mapping = get(values, name); - arrayHelpers.insert(mapping.length, initial); + arrayHelpers.insert(mapping ? mapping.length : 0, initial); }} > diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js b/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js similarity index 62% rename from src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js rename to src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js index a7a323cb8b..3cf9cf3d97 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/AuthAttributePath.js +++ b/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js @@ -1,26 +1,19 @@ import {useField} from 'formik'; -import {useContext} from 'react'; +import PropTypes from 'prop-types'; import {FormattedMessage} from 'react-intl'; -import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import ArrayInput from 'components/admin/forms/ArrayInput'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; -const AuthAttributePath = () => { - const [fieldProps, , fieldHelpers] = useField({name: 'authAttributePath', type: 'array'}); - const [updateExistingObject] = useField('updateExistingObject'); +const AuthAttributePath = ({name, required = true, ...extraProps}) => { + const [fieldProps, , fieldHelpers] = useField({name: name, type: 'array'}); const {setValue} = fieldHelpers; - const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = - useContext(FeatureFlagsContext); - - if (!REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION) { - return null; - } return ( { defaultMessage="This is used to perform validation to verify that the authenticated user is the owner of the object." /> } - required={!!updateExistingObject.value} + required={required} > { setValue(value); }} inputType="text" + {...extraProps} /> ); }; -AuthAttributePath.propTypes = {}; +AuthAttributePath.propTypes = { + name: PropTypes.string.isRequired, + required: PropTypes.bool, +}; export default AuthAttributePath; diff --git a/src/openforms/js/components/admin/forms/objects_api/index.js b/src/openforms/js/components/admin/forms/objects_api/index.js index 0d50dcfab0..01c8607560 100644 --- a/src/openforms/js/components/admin/forms/objects_api/index.js +++ b/src/openforms/js/components/admin/forms/objects_api/index.js @@ -1,3 +1,4 @@ +export {default as AuthAttributePath} from './AuthAttributePath'; export {default as ObjectsAPIGroup} from './ObjectsAPIGroup'; export {default as ObjectTypeSelect} from './ObjectTypeSelect'; export {default as ObjectTypeVersionSelect} from './ObjectTypeVersionSelect'; From 3616e7984b5fcc3090e1c50512bdcc15b2231868 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 25 Nov 2024 12:46:47 +0100 Subject: [PATCH 18/39] :globe_with_meridians: [#4398] Compile JS translations --- src/openforms/js/compiled-lang/en.json | 12 ++++++++++++ src/openforms/js/compiled-lang/nl.json | 12 ++++++++++++ src/openforms/js/lang/en.json | 10 ++++++++++ src/openforms/js/lang/nl.json | 10 ++++++++++ 4 files changed, 44 insertions(+) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 19f6fc0f02..2426de61dd 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -233,6 +233,12 @@ "value": "Confirm" } ], + "1Hb/pK": [ + { + "type": 0, + "value": "This is used to perform validation to verify that the authenticated user is the owner of the object." + } + ], "1HfdUc": [ { "type": 0, @@ -5361,6 +5367,12 @@ "value": "Partner 1" } ], + "lu7yMK": [ + { + "type": 0, + "value": "Path to auth attribute (e.g. BSN/KVK) in objects" + } + ], "m20av3": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 0c6c0e3eca..2728037ed2 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -233,6 +233,12 @@ "value": "Bevestigen" } ], + "1Hb/pK": [ + { + "type": 0, + "value": "This is used to perform validation to verify that the authenticated user is the owner of the object." + } + ], "1HfdUc": [ { "type": 0, @@ -5383,6 +5389,12 @@ "value": "Partner 1" } ], + "lu7yMK": [ + { + "type": 0, + "value": "Path to auth attribute (e.g. BSN/KVK) in objects" + } + ], "m20av3": [ { "type": 0, diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json index fdcbd2d0b0..9b8e40ce86 100644 --- a/src/openforms/js/lang/en.json +++ b/src/openforms/js/lang/en.json @@ -109,6 +109,11 @@ "description": "Camunda complex process variables confirm button", "originalDefault": "Confirm" }, + "1Hb/pK": { + "defaultMessage": "This is used to perform validation to verify that the authenticated user is the owner of the object.", + "description": "Objects API registration: authAttributePath helpText", + "originalDefault": "This is used to perform validation to verify that the authenticated user is the owner of the object." + }, "1HfdUc": { "defaultMessage": "The API group specifies which objects and objecttypes services to use.", "description": "Objects API group field help text", @@ -2509,6 +2514,11 @@ "description": "ZGW APIs registration options 'objecttype' help text", "originalDefault": "URL to the object type in the objecttypes API. If provided, an object will be created and a case object relation will be added to the case." }, + "lu7yMK": { + "defaultMessage": "Path to auth attribute (e.g. BSN/KVK) in objects", + "description": "Objects API registration: authAttributePath label", + "originalDefault": "Path to auth attribute (e.g. BSN/KVK) in objects" + }, "m83ECr": { "defaultMessage": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?", "description": "Objects API prefill configuration: warning message when copying the config from registration backend", diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json index fcd8b35b7b..06b9f74fe8 100644 --- a/src/openforms/js/lang/nl.json +++ b/src/openforms/js/lang/nl.json @@ -110,6 +110,11 @@ "description": "Camunda complex process variables confirm button", "originalDefault": "Confirm" }, + "1Hb/pK": { + "defaultMessage": "This is used to perform validation to verify that the authenticated user is the owner of the object.", + "description": "Objects API registration: authAttributePath helpText", + "originalDefault": "This is used to perform validation to verify that the authenticated user is the owner of the object." + }, "1HfdUc": { "defaultMessage": "De API-groep bepaalt welke services voor de objecten- en objecttypen-API's gebruikt wordt.", "description": "Objects API group field help text", @@ -2530,6 +2535,11 @@ "description": "ZGW APIs registration options 'objecttype' help text", "originalDefault": "URL to the object type in the objecttypes API. If provided, an object will be created and a case object relation will be added to the case." }, + "lu7yMK": { + "defaultMessage": "Path to auth attribute (e.g. BSN/KVK) in objects", + "description": "Objects API registration: authAttributePath label", + "originalDefault": "Path to auth attribute (e.g. BSN/KVK) in objects" + }, "m83ECr": { "defaultMessage": "Wanneer je de instellingen van een registratiepluginoptie overneemt, dan worden alle bestaande instellingen overschreven. Ben je zeker dat je door wil gaan?", "description": "Objects API prefill configuration: warning message when copying the config from registration backend", From 94a16e7399304264f882f54dfd1801f0e19f8da5 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 25 Nov 2024 15:06:47 +0100 Subject: [PATCH 19/39] :sparkles: [#4398] Display errors for authAttributePath in modal --- .../variables/VariablesEditor.stories.js | 60 +++++++++++++++++++ .../variables/prefill/PrefillSummary.js | 23 ++++++- .../prefill/objects_api/ObjectsAPIFields.js | 9 ++- .../forms/objects_api/AuthAttributePath.js | 3 +- .../contrib/objects_api/api/serializers.py | 9 +++ 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index b2802c1656..632d6e4a7d 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -799,6 +799,66 @@ export const WithValidationErrors = { }, }; +export const ConfigurePrefillObjectsAPIWithValidationErrors = { + args: { + variables: [ + { + form: 'http://localhost:8000/api/v2/forms/36612390', + formDefinition: undefined, + name: 'User defined', + key: 'userDefined', + source: 'user_defined', + prefillPlugin: 'objects_api', + prefillAttribute: '', + prefillIdentifierRole: '', + dataType: 'string', + dataFormat: undefined, + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: [], + options: { + objectsApiGroup: 1, + objecttype: '2c77babf-a967-4057-9969-0200320d23f1', + objecttypeVersion: 2, + authAttributePath: ['path', 'to', 'bsn'], + variablesMapping: [ + { + variableKey: 'formioComponent', + targetPath: ['height'], + }, + { + variableKey: 'userDefined', + targetPath: ['species'], + }, + ], + }, + errors: { + prefillOptions: {authAttributePath: 'This list may not be empty.'}, + }, + }, + ], + }, + play: async ({canvasElement, step}) => { + const canvas = within(canvasElement); + + await step('Open configuration modal', async () => { + const userDefinedVarsTab = await canvas.findByRole('tab', {name: 'Gebruikersvariabelen'}); + expect(userDefinedVarsTab).toBeVisible(); + await userEvent.click(userDefinedVarsTab); + + // open modal for configuration + const editIcon = canvas.getByTitle('Prefill instellen'); + await userEvent.click(editIcon); + expect(await canvas.findByRole('dialog')).toBeVisible(); + }); + + await step('Verify that error is shown', async () => { + const error = canvas.getByText('This list may not be empty.'); + expect(error).toBeVisible(); + }); + }, +}; + export const AddressNLMappingSpecificTargetsNoDeriveAddress = { args: { variables: [ diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js index 72b59b3ed5..17c99769f3 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js @@ -10,6 +10,20 @@ import ErrorBoundary from 'components/errors/ErrorBoundary'; import {IDENTIFIER_ROLE_CHOICES} from '../constants'; import PrefillConfigurationForm from './PrefillConfigurationForm'; +function isTruthy(value) { + if (value === undefined || value === null) { + return false; + } + + // Check if the value is an object + if (typeof value === 'object') { + // Check if it's an empty object + if (Object.keys(value).length === 0) { + return false; + } + return true; + } +} const PrefillSummary = ({ plugin = '', attribute = '', @@ -29,7 +43,11 @@ const PrefillSummary = ({ intl ); - const hasErrors = hasPluginErrors || hasAttributeErrors || hasIdentifierRoleErrors; + const hasErrors = + hasPluginErrors || + hasAttributeErrors || + hasIdentifierRoleErrors || + isTruthy(errors.prefillOptions); const icons = (
@@ -100,6 +118,9 @@ const PrefillSummary = ({ plugin: pluginErrors, attribute: attributeErrors, identifierRole: identifierRoleErrors, + // Directly pass these without normalizing, because the shape + // depends on the plugin that is used + options: errors.prefillOptions, }} /> diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index b077f4490a..a50e3b53c9 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -11,6 +11,7 @@ import useAsync from 'react-use/esm/useAsync'; import {FormContext} from 'components/admin/form_design/Context'; import useConfirm from 'components/admin/form_design/useConfirm'; +import {normalizeErrors} from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {LOADING_OPTION} from 'components/admin/forms/Select'; @@ -127,6 +128,8 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { if (error) throw error; const prefillProperties = loading ? LOADING_OPTION : value; + const [, authAttributePathErrors] = normalizeErrors(errors.options?.authAttributePath, intl); + return ( <> {showCopyButton ? ( @@ -204,7 +207,11 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { objectTypeFieldName="options.objecttypeUuid" /> - +
{ +const AuthAttributePath = ({name, errors, required = true, ...extraProps}) => { const [fieldProps, , fieldHelpers] = useField({name: name, type: 'array'}); const {setValue} = fieldHelpers; @@ -14,6 +14,7 @@ const AuthAttributePath = ({name, required = true, ...extraProps}) => { Date: Tue, 26 Nov 2024 15:40:57 +0100 Subject: [PATCH 20/39] :recycle: [#4398] Use setValues instead of repeating setFieldValue when resetting values in the Objects API prefill form --- .../prefill/objects_api/ObjectsAPIFields.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index a50e3b53c9..e876c24774 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -72,6 +72,7 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { }, }, setFieldValue, + setValues, } = useFormikContext(); const defaults = { @@ -145,8 +146,16 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { if (!objecttypeUuid) return true; const confirmSwitch = await openApiGroupConfirmationModal(); if (!confirmSwitch) return false; - setFieldValue('options.authAttributePath', []); - setFieldValue('options.variablesMapping', []); + setValues(prevValues => ({ + ...prevValues, + // Trying to set multiple nested values doesn't work, since it sets them + // with dots in the key + options: { + ...prevValues.options, + authAttributePath: [], + variablesMapping: [], + }, + })); return true; }} name="options.objectsApiGroup" @@ -180,8 +189,16 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { if (values.options.variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmationModal(); if (!confirmSwitch) return false; - setFieldValue('options.authAttributePath', []); - setFieldValue('options.variablesMapping', []); + setValues(prevValues => ({ + ...prevValues, + // Trying to set multiple nested values doesn't work, since it sets them + // with dots in the key + options: { + ...prevValues.options, + authAttributePath: [], + variablesMapping: [], + }, + })); return true; }} /> From bd1cc701b78828dda32516e58258fcd43f78de09 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 26 Nov 2024 14:25:09 +0100 Subject: [PATCH 21/39] :ok_hand: [#4398] Process PR feedback * several cleanups of code * also raise errors in prefill scenario * explicitly raise error if auth attribute cannot be found at path * add contextmanager to use VCR in setUpTestData * remove REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION feature flag (already added in 2.8.x) * add separate fieldset for updateExistingObject in registration fields * make authAttributePath disabled instead of not required if updateExistingObject is false * use PropTypes.node instead of PropTypes.string for Objects API fields helptexts/labels --- src/openforms/conf/base.py | 9 --- src/openforms/conf/dev.py | 5 -- .../objects_api/ownership_validation.py | 48 +++++++++------ ..._without_options_does_not_raise_error.yaml | 6 +- ...kends_configured_does_not_raise_error.yaml | 6 +- ...ests.test_user_is_not_owner_of_object.yaml | 6 +- ...owner_of_object_nested_auth_attribute.yaml | 14 ++--- ...torTests.test_user_is_owner_of_object.yaml | 6 +- .../setUpTestData.yaml | 8 +-- .../tests/test_ownership_validation.py | 20 ++----- src/openforms/forms/admin/mixins.py | 4 -- .../components/admin/form_design/Context.js | 1 - .../form_design/RegistrationFields.stories.js | 16 ++--- .../objectsapi/LegacyConfigFields.js | 58 +++++++++++-------- .../objectsapi/ObjectsApiOptionsForm.js | 2 +- .../ObjectsApiOptionsFormFields.stories.js | 5 +- .../objectsapi/V2ConfigFields.js | 57 ++++++++++-------- .../objectsapi/fields/UpdateExistingObject.js | 8 --- .../admin/form_design/story-decorators.js | 3 - .../variables/prefill/PrefillSummary.js | 15 +---- .../prefill/objects_api/ObjectsAPIFields.js | 31 +++++----- .../forms/objects_api/AuthAttributePath.js | 4 +- .../forms/objects_api/ObjectTypeSelect.js | 4 +- .../objects_api/ObjectTypeVersionSelect.js | 2 +- src/openforms/prefill/base.py | 2 + .../prefill/contrib/objects_api/plugin.py | 1 + ...d_if_initial_data_reference_specified.yaml | 6 +- .../setUpTestData.yaml | 8 +-- .../test_initial_data_ownership_validation.py | 14 +---- src/openforms/prefill/sources.py | 10 ++-- .../contrib/objects_api/plugin.py | 1 + src/openforms/utils/tests/vcr.py | 18 ++++++ 32 files changed, 196 insertions(+), 202 deletions(-) diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index 3d181e8c9d..ac4d69afe7 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -1203,15 +1203,6 @@ "value": config("ZGW_APIS_INCLUDE_DRAFTS", default=False), }, ], - "REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION": [ - { - "condition": "boolean", - "value": config( - "REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION", - default=False, - ), - }, - ], } # diff --git a/src/openforms/conf/dev.py b/src/openforms/conf/dev.py index 749c023627..3f4127c530 100644 --- a/src/openforms/conf/dev.py +++ b/src/openforms/conf/dev.py @@ -30,11 +30,6 @@ os.environ.setdefault("SENDFILE_BACKEND", "django_sendfile.backends.development") -# Feature flags for development -os.environ.setdefault( - "REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION", - "1", -) from .base import * # noqa isort:skip diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py index 34729843d3..d518d513bc 100644 --- a/src/openforms/contrib/objects_api/ownership_validation.py +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -3,9 +3,9 @@ import logging from typing import TYPE_CHECKING -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import PermissionDenied -from glom import Path, glom +from glom import Path, PathAccessError, glom from requests.exceptions import RequestException from openforms.contrib.objects_api.clients import ObjectsClient @@ -14,10 +14,8 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from openforms.prefill.contrib.objects_api.plugin import ObjectsAPIPrefill - from openforms.registrations.contrib.objects_api.plugin import ( - ObjectsAPIRegistration, - ) + from openforms.prefill.base import BasePlugin as BasePrefillPlugin + from openforms.registrations.base import BasePlugin as BaseRegistrationPlugin from openforms.submissions.models import Submission @@ -25,7 +23,8 @@ def validate_object_ownership( submission: Submission, client: ObjectsClient, object_attribute: list[str], - plugin: ObjectsAPIPrefill | ObjectsAPIRegistration, + plugin: BasePrefillPlugin | BaseRegistrationPlugin, + raise_exception: bool = True, ) -> None: """ Function to check whether the user associated with a Submission is the owner @@ -35,39 +34,52 @@ def validate_object_ownership( """ assert submission.initial_data_reference - try: - auth_info = submission.auth_info - except ObjectDoesNotExist: - logger.exception( + if not submission.is_authenticated: + logger.warning( "Cannot perform object ownership validation for reference %s with unauthenticated user", submission.initial_data_reference, ) logevent.object_ownership_check_anonymous_user(submission, plugin=plugin) raise PermissionDenied("Cannot pass data reference as anonymous user") + auth_info = submission.auth_info + object = None try: object = client.get_object(submission.initial_data_reference) - except RequestException: + except RequestException as e: logger.exception( "Something went wrong while trying to retrieve " "object for initial_data_reference" ) + if raise_exception: + raise PermissionDenied from e if not object: # If the object cannot be found, we cannot consider the ownership check failed # because it is not verified that the user is not the owner - logger.info( + logger.warning( "Could not find object for initial_data_reference: %s", submission.initial_data_reference, ) - return + if raise_exception: + raise PermissionDenied("Could not find object for initial_data_reference") + else: + return - if ( - glom(object["record"]["data"], Path(*object_attribute), default=None) - != auth_info.value - ): + try: + auth_value = glom(object["record"]["data"], Path(*object_attribute)) + except PathAccessError as e: logger.exception( + "Could not retrieve auth value for path %s, it could be incorrectly configured", + object_attribute, + ) + raise PermissionDenied( + "Could not verify if user is owner of the referenced object" + ) from e + + if auth_value != auth_info.value: + logger.warning( "Submission with initial_data_reference did not pass ownership check for reference %s", submission.initial_data_reference, ) diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml index fe396c8123..955b212c8a 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_error.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 + uri: http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf","uuid":"0122126f-4a7f-49d4-b131-b83786e15acf","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:43 GMT + - Tue, 26 Nov 2024 13:21:59 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml index fe396c8123..955b212c8a 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_error.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 + uri: http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf","uuid":"0122126f-4a7f-49d4-b131-b83786e15acf","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:43 GMT + - Tue, 26 Nov 2024 13:21:59 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml index 9422e59933..955b212c8a 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 + uri: http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf","uuid":"0122126f-4a7f-49d4-b131-b83786e15acf","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:21:59 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml index eee9ce3845..45e5b03a94 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_not_owner_of_object_nested_auth_attribute.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"nested": {"bsn": "111222333"}, "foo": - "bar"}, "startAt": "2024-11-05"}}' + "bar"}, "startAt": "2024-11-26"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95","uuid":"0fd71d89-3ec2-4593-8fb5-9e1822203c95","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/ebae7217-7a91-43bb-bfce-82c353e46289","uuid":"ebae7217-7a91-43bb-bfce-82c353e46289","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:21:59 GMT Location: - - http://localhost:8002/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95 + - http://localhost:8002/api/v2/objects/ebae7217-7a91-43bb-bfce-82c353e46289 Referrer-Policy: - same-origin Server: @@ -71,10 +71,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95 + uri: http://localhost:8002/api/v2/objects/ebae7217-7a91-43bb-bfce-82c353e46289 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/0fd71d89-3ec2-4593-8fb5-9e1822203c95","uuid":"0fd71d89-3ec2-4593-8fb5-9e1822203c95","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/ebae7217-7a91-43bb-bfce-82c353e46289","uuid":"ebae7217-7a91-43bb-bfce-82c353e46289","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -89,7 +89,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:22:00 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml index 9422e59933..20c1a566aa 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_user_is_owner_of_object.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 + uri: http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf","uuid":"0122126f-4a7f-49d4-b131-b83786e15acf","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:22:00 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml index 0003a7470f..381d3cafcd 100644 --- a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"bsn": "111222333", "foo": "bar"}, "startAt": - "2024-11-05"}}' + "2024-11-26"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506","uuid":"300e61aa-e150-459c-aa30-d3f0fe4f5506","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf","uuid":"0122126f-4a7f-49d4-b131-b83786e15acf","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","foo":"bar"},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:43 GMT + - Tue, 26 Nov 2024 13:21:59 GMT Location: - - http://localhost:8002/api/v2/objects/300e61aa-e150-459c-aa30-d3f0fe4f5506 + - http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf Referrer-Policy: - same-origin Server: diff --git a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 74e4cd2244..3f4ca918d8 100644 --- a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -5,7 +5,6 @@ from django.test import TestCase, override_settings, tag from requests.exceptions import RequestException -from vcr.config import VCR from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client @@ -15,7 +14,7 @@ from openforms.logging.models import TimelineLogProxy from openforms.registrations.contrib.objects_api.plugin import ObjectsAPIRegistration from openforms.submissions.tests.factories import SubmissionFactory -from openforms.utils.tests.vcr import OFVCRMixin +from openforms.utils.tests.vcr import OFVCRMixin, with_setup_test_data_vcr from ..ownership_validation import validate_object_ownership @@ -41,15 +40,7 @@ def setUpTestData(cls): for_test_docker_compose=True ) - # Explicitly define a cassette for Object creation, because running this in - # setUpTestData doesn't record cassettes by default - cassette_path = Path( - cls.VCR_TEST_FILES - / "vcr_cassettes" - / cls.__qualname__ - / "setUpTestData.yaml" - ) - with VCR().use_cassette(cassette_path): + with with_setup_test_data_vcr(cls.VCR_TEST_FILES, cls.__qualname__): with get_objects_client(cls.objects_api_group_used) as client: object = client.create_object( record_data=prepare_data_for_registration( @@ -193,7 +184,7 @@ def test_user_is_not_owner_of_object_nested_auth_attribute(self): def test_request_exception_when_doing_permission_check(self, mock_get_object): """ If the object could not be fetched due to request errors, the ownership check - should not fail + should fail """ submission = SubmissionFactory.create( auth_info__value="111222333", @@ -213,8 +204,9 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): }, ) - with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) + with self.assertRaises(PermissionDenied): + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"], PLUGIN) @tag("gh-4398") def test_no_backends_configured_does_not_raise_error( diff --git a/src/openforms/forms/admin/mixins.py b/src/openforms/forms/admin/mixins.py index 694fdb68e6..84000b2003 100644 --- a/src/openforms/forms/admin/mixins.py +++ b/src/openforms/forms/admin/mixins.py @@ -34,10 +34,6 @@ def render_change_form( "ZGW_APIS_INCLUDE_DRAFTS": flag_enabled( "ZGW_APIS_INCLUDE_DRAFTS", request=request ), - "REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION": flag_enabled( - "REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION", - request=request, - ), }, "confidentiality_levels": [ {"label": label, "value": value} diff --git a/src/openforms/js/components/admin/form_design/Context.js b/src/openforms/js/components/admin/form_design/Context.js index 8789fa2085..c87ccc2d7d 100644 --- a/src/openforms/js/components/admin/form_design/Context.js +++ b/src/openforms/js/components/admin/form_design/Context.js @@ -5,7 +5,6 @@ TinyMceContext.displayName = 'TinyMceContext'; const FeatureFlagsContext = React.createContext({ ZGW_APIS_INCLUDE_DRAFTS: false, - REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION: false, }); FeatureFlagsContext.displayName = 'FeatureFlagsContext'; diff --git a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js index 4bd397d8d0..0d28050491 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -15,7 +15,6 @@ import { mockRoleTypesGet as mockZGWApisRoleTypesGet, } from 'components/admin/form_design/registrations/zgw/mocks'; import { - FeatureFlagsDecorator, FormDecorator, ValidationErrorsDecorator, } from 'components/admin/form_design/story-decorators'; @@ -25,7 +24,7 @@ import RegistrationFields from './RegistrationFields'; export default { title: 'Form design / Registration / RegistrationFields', - decorators: [FeatureFlagsDecorator, ValidationErrorsDecorator, FormDecorator], + decorators: [ValidationErrorsDecorator, FormDecorator], component: RegistrationFields, args: { availableBackends: [ @@ -690,11 +689,6 @@ export const ConfiguredBackends = { }; export const ObjectsAPI = { - parameters: { - featureFlags: { - REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION: true, - }, - }, args: { configuredBackends: [ { @@ -750,7 +744,7 @@ export const ObjectsAPI = { 'Path to auth attribute is required if updating existing objects is enabled', async () => { const otherSettingsTitle = modal.getByRole('heading', { - name: 'Overige instellingen (Tonen)', + name: 'Update existing objects (Tonen)', }); expect(otherSettingsTitle).toBeVisible(); await userEvent.click(within(otherSettingsTitle).getByRole('link', {name: '(Tonen)'})); @@ -759,13 +753,13 @@ export const ObjectsAPI = { 'Path to auth attribute (e.g. BSN/KVK) in objects' ); - expect(authAttributePath).not.toHaveClass('required'); + expect(authAttributePath.parentElement.parentElement).toHaveClass('field--disabled'); const updateExistingObject = modal.getByLabelText('Bestaand object bijwerken'); await userEvent.click(updateExistingObject); - // Checking `updateExistingObject` should make `authAttributePath` required - expect(authAttributePath).toHaveClass('required'); + // Checking `updateExistingObject` should make `authAttributePath` no longer disabled + expect(authAttributePath.parentElement.parentElement).not.toHaveClass('field--disabled'); } ); diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js index ba80b822f9..a572326e02 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js @@ -1,9 +1,7 @@ import {useField} from 'formik'; import PropTypes from 'prop-types'; -import {useContext} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; -import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; @@ -34,12 +32,8 @@ const onApiGroupChange = prevValues => ({ }); const LegacyConfigFields = ({apiGroupChoices}) => { - const intl = useIntl(); - const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = - useContext(FeatureFlagsContext); - const [updateExistingObject] = useField('updateExistingObject'); - const authAttributePathRequired = !!updateExistingObject.value; + const authAttributePathDisabled = !updateExistingObject.value; return ( <> @@ -55,20 +49,26 @@ const LegacyConfigFields = ({apiGroupChoices}) => { } > + } + helpText={ + + } /> + } />
@@ -101,6 +101,20 @@ const LegacyConfigFields = ({apiGroupChoices}) => { +
+ } + collapsible + fieldNames={['updateExistingObject', 'authAttributePath']} + > + + +
+
{ fieldNames={['organisatieRsin']} > - - {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION ? ( - - ) : null}
diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js index 586518e70c..c10baf9f36 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsForm.js @@ -56,7 +56,7 @@ ObjectsApiOptionsForm.propTypes = { objecttype: PropTypes.string, objecttypeVersion: PropTypes.number, updateExistingObject: PropTypes.bool, - authAttributePath: PropTypes.array, + authAttributePath: PropTypes.arrayOf(PropTypes.string), productaanvraagType: PropTypes.string, informatieobjecttypeSubmissionReport: PropTypes.string, uploadSubmissionCsv: PropTypes.bool, diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js index ddee0bdd9d..df9ddc3a3e 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js @@ -38,9 +38,6 @@ export default { formData: {}, }, parameters: { - featureFlags: { - REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION: true, - }, msw: { handlers: [ mockObjecttypesGet([ @@ -312,6 +309,7 @@ export const V1ValidationErrors = { [`${NAME}.informatieobjecttypeSubmissionReport`, 'Computer says no'], [`${NAME}.informatieobjecttypeSubmissionCsv`, 'Computer says no'], [`${NAME}.informatieobjecttypeAttachment`, 'Computer says no'], + [`${NAME}.authAttributePath`, 'Field is required'], [`${NAME}.organisatieRsin`, 'Computer says no'], ], }, @@ -329,6 +327,7 @@ export const V2ValidationErrors = { [`${NAME}.informatieobjecttypeSubmissionReport`, 'Computer says no'], [`${NAME}.informatieobjecttypeSubmissionCsv`, 'Computer says no'], [`${NAME}.informatieobjecttypeAttachment`, 'Computer says no'], + [`${NAME}.authAttributePath`, 'Field is required'], [`${NAME}.organisatieRsin`, 'Computer says no'], ], }, diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js index b5f67d4ecd..8401900171 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js @@ -1,10 +1,7 @@ import {useField, useFormikContext} from 'formik'; import PropTypes from 'prop-types'; -import {useContext} from 'react'; -import {useIntl} from 'react-intl'; import {FormattedMessage} from 'react-intl'; -import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import useConfirm from 'components/admin/form_design/useConfirm'; import Fieldset from 'components/admin/forms/Fieldset'; import { @@ -34,12 +31,8 @@ const onApiGroupChange = prevValues => ({ }); const V2ConfigFields = ({apiGroupChoices}) => { - const intl = useIntl(); - const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = - useContext(FeatureFlagsContext); - const [updateExistingObject] = useField('updateExistingObject'); - const authAttributePathRequired = !!updateExistingObject.value; + const authAttributePathDisabled = !updateExistingObject.value; const { values: {variablesMapping = []}, @@ -80,14 +73,18 @@ const V2ConfigFields = ({apiGroupChoices}) => { } > + } + helpText={ + + } onChangeCheck={async () => { if (variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmation(); @@ -107,10 +104,12 @@ const V2ConfigFields = ({apiGroupChoices}) => { } > + } /> @@ -128,6 +127,20 @@ const V2ConfigFields = ({apiGroupChoices}) => { +
+ } + collapsible + fieldNames={['updateExistingObject', 'authAttributePath']} + > + + +
+
{ fieldNames={['organisatieRsin']} > - - {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION ? ( - - ) : null}
diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/UpdateExistingObject.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/UpdateExistingObject.js index 3f5dc0ba3c..3b26ccf77c 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/UpdateExistingObject.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/fields/UpdateExistingObject.js @@ -1,19 +1,11 @@ import {useField} from 'formik'; -import {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; -import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import FormRow from 'components/admin/forms/FormRow'; import {Checkbox} from 'components/admin/forms/Inputs'; const UpdateExistingObject = () => { const [fieldProps] = useField({name: 'updateExistingObject', type: 'checkbox'}); - const {REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION = false} = - useContext(FeatureFlagsContext); - - if (!REGISTRATION_OBJECTS_API_ENABLE_EXISTING_OBJECT_INTEGRATION) { - return null; - } return ( diff --git a/src/openforms/js/components/admin/form_design/story-decorators.js b/src/openforms/js/components/admin/form_design/story-decorators.js index 208ea5bb43..fad8d5f766 100644 --- a/src/openforms/js/components/admin/form_design/story-decorators.js +++ b/src/openforms/js/components/admin/form_design/story-decorators.js @@ -12,9 +12,6 @@ export const FeatureFlagsDecorator = (Story, {parameters}) => ( diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js index 17c99769f3..a6265f56b4 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillSummary.js @@ -11,19 +11,10 @@ import {IDENTIFIER_ROLE_CHOICES} from '../constants'; import PrefillConfigurationForm from './PrefillConfigurationForm'; function isTruthy(value) { - if (value === undefined || value === null) { - return false; - } - - // Check if the value is an object - if (typeof value === 'object') { - // Check if it's an empty object - if (Object.keys(value).length === 0) { - return false; - } - return true; - } + if (!value) return false; + return Object.keys(value).length > 0; } + const PrefillSummary = ({ plugin = '', attribute = '', diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index e876c24774..a50b145fe1 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -176,15 +176,18 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { name="options.objecttypeUuid" apiGroupFieldName="options.objectsApiGroup" versionFieldName="options.objecttypeVersion" - label={intl.formatMessage({ - description: "Objects API prefill options 'Objecttype' label", - defaultMessage: 'Objecttype', - })} - helpText={intl.formatMessage({ - description: "Objects API prefill options 'Objecttype' helpText", - defaultMessage: - 'The prefill values will be taken from an object of the selected type.', - })} + label={ + + } + helpText={ + + } onChangeCheck={async () => { if (values.options.variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmationModal(); @@ -216,10 +219,12 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { > + } apiGroupFieldName="options.objectsApiGroup" objectTypeFieldName="options.objecttypeUuid" /> diff --git a/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js b/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js index 02acf0b803..89d6bceb5c 100644 --- a/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js +++ b/src/openforms/js/components/admin/forms/objects_api/AuthAttributePath.js @@ -6,7 +6,7 @@ import ArrayInput from 'components/admin/forms/ArrayInput'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; -const AuthAttributePath = ({name, errors, required = true, ...extraProps}) => { +const AuthAttributePath = ({name, errors, disabled = false, ...extraProps}) => { const [fieldProps, , fieldHelpers] = useField({name: name, type: 'array'}); const {setValue} = fieldHelpers; @@ -27,7 +27,7 @@ const AuthAttributePath = ({name, errors, required = true, ...extraProps}) => { defaultMessage="This is used to perform validation to verify that the authenticated user is the owner of the object." /> } - required={required} + disabled={disabled} > None: + assert submission.initial_data_reference api_group = ObjectsAPIGroupConfig.objects.filter( pk=prefill_options.get("objects_api_group") ).first() diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml index b7527eec5a..614f90c9f6 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff + uri: http://localhost:8002/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff","uuid":"d23144bd-1220-42e6-9edf-d7fd31291bff","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952","uuid":"bd6a9316-f721-40e4-9d4d-73ada5590952","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,7 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:21:31 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml index cea8230065..2189fc88bf 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, - "startAt": "2024-11-05"}}' + "startAt": "2024-11-26"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff","uuid":"d23144bd-1220-42e6-9edf-d7fd31291bff","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952","uuid":"bd6a9316-f721-40e4-9d4d-73ada5590952","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,9 +39,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:28:44 GMT + - Tue, 26 Nov 2024 13:21:30 GMT Location: - - http://localhost:8002/api/v2/objects/d23144bd-1220-42e6-9edf-d7fd31291bff + - http://localhost:8002/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952 Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 0c6c852ef5..e1dc1c227e 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -4,8 +4,6 @@ from django.core.exceptions import PermissionDenied from django.test import TestCase, tag -from vcr.config import VCR - from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.helpers import prepare_data_for_registration @@ -20,7 +18,7 @@ from openforms.logging.models import TimelineLogProxy from openforms.prefill.service import prefill_variables from openforms.submissions.tests.factories import SubmissionStepFactory -from openforms.utils.tests.vcr import OFVCRMixin +from openforms.utils.tests.vcr import OFVCRMixin, with_setup_test_data_vcr TEST_FILES = (Path(__file__).parent / "files").resolve() @@ -37,15 +35,7 @@ def setUpTestData(cls): for_test_docker_compose=True ) - # Explicitly define a cassette for Object creation, because running this in - # setUpTestData doesn't record cassettes by default - cassette_path = Path( - cls.VCR_TEST_FILES - / "vcr_cassettes" - / cls.__qualname__ - / "setUpTestData.yaml" - ) - with VCR().use_cassette(cassette_path): + with with_setup_test_data_vcr(cls.VCR_TEST_FILES, cls.__qualname__): with get_objects_client(cls.objects_api_group_used) as client: object = client.create_object( record_data=prepare_data_for_registration( diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index eef6438e2f..9c309e26c6 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -101,14 +101,14 @@ def fetch_prefill_values_from_options( # If an `initial_data_reference` was passed, we must verify that the # authenticated user is the owner of the referenced object - try: - if submission.initial_data_reference: + if submission.initial_data_reference: + try: plugin.verify_initial_data_ownership( submission, variable.form_variable.prefill_options ) - except (PermissionDenied, ImproperlyConfigured) as exc: - logevent.prefill_retrieve_failure(submission, plugin, exc) - continue + except (PermissionDenied, ImproperlyConfigured) as exc: + logevent.prefill_retrieve_failure(submission, plugin, exc) + continue options_serializer = plugin.options(data=variable.form_variable.prefill_options) diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 39cd8640ee..102cce7e91 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -177,6 +177,7 @@ def get_variables(self) -> list[FormVariable]: def verify_initial_data_ownership(self, submission: Submission) -> None: assert submission.registration_backend + assert submission.initial_data_reference backend = submission.registration_backend api_group = ObjectsAPIGroupConfig.objects.filter( diff --git a/src/openforms/utils/tests/vcr.py b/src/openforms/utils/tests/vcr.py index 0cf1922a35..49eb6a86c4 100644 --- a/src/openforms/utils/tests/vcr.py +++ b/src/openforms/utils/tests/vcr.py @@ -56,8 +56,11 @@ # once in dev, none in CI import os +from contextlib import contextmanager from pathlib import Path +from typing import Iterator +from vcr.config import VCR from vcr.unittest import VCRMixin RECORD_MODE = os.environ.get("VCR_RECORD_MODE", "none") @@ -85,3 +88,18 @@ def _get_vcr_kwargs(self): kwargs = super()._get_vcr_kwargs() kwargs.setdefault("record_mode", RECORD_MODE) return kwargs + + +@contextmanager +def with_setup_test_data_vcr(base_path: Path | str, class_name: str) -> Iterator[None]: + """ + Context manager to explicitly use VCR (inside setUpTestData for instance) + + :param base_path: The base directory for VCR test files. + :param class_name: The qualified name of the test class. + """ + cassette_path = ( + Path(base_path) / "vcr_cassettes" / class_name / "setUpTestData.yaml" + ) + with VCR().use_cassette(cassette_path): + yield From 04cee6145609859ca7484e6ee6e41e094a24c412 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 28 Nov 2024 16:03:57 +0100 Subject: [PATCH 22/39] :recycle: [#4398] Use TargetPathSelect for auth attribute path --- ...NlObjectsApiVariableConfigurationEditor.js | 3 +- ...icObjectsApiVariableConfigurationEditor.js | 54 +--------------- .../objectsapi/LegacyConfigFields.js | 14 ++++- .../ObjectsApiVariableConfigurationEditor.js | 21 +------ .../objectsapi/V2ConfigFields.js | 10 ++- .../variables/VariablesEditor.stories.js | 49 +++++++++++++-- .../prefill/objects_api/ObjectsAPIFields.js | 5 +- .../js/components/admin/forms/ArrayInput.js | 8 +-- .../forms/objects_api/AuthAttributePath.js | 61 ++++++++++++++----- .../forms/objects_api/TargetPathDisplay.js | 23 +++++++ .../forms/objects_api/TargetPathSelect.js | 54 ++++++++++++++++ .../admin/forms/objects_api/index.js | 2 + 12 files changed, 201 insertions(+), 103 deletions(-) create mode 100644 src/openforms/js/components/admin/forms/objects_api/TargetPathDisplay.js create mode 100644 src/openforms/js/components/admin/forms/objects_api/TargetPathSelect.js diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js index 47c8b75a30..7f9265580b 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js @@ -12,11 +12,10 @@ import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {Checkbox} from 'components/admin/forms/Inputs'; import Select, {LOADING_OPTION} from 'components/admin/forms/Select'; +import {TargetPathDisplay} from 'components/admin/forms/objects_api'; import ErrorMessage from 'components/errors/ErrorMessage'; import {post} from 'utils/fetch'; -import {TargetPathDisplay} from './ObjectsApiVariableConfigurationEditor'; - const ADDRESSNL_NESTED_PROPERTIES = { postcode: {type: 'string'}, houseLetter: {type: 'string'}, diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/GenericObjectsApiVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/GenericObjectsApiVariableConfigurationEditor.js index aa0fd50a92..9c8259dec2 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/GenericObjectsApiVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/GenericObjectsApiVariableConfigurationEditor.js @@ -1,6 +1,5 @@ -import {FieldArray, useFormikContext} from 'formik'; +import {useFormikContext} from 'formik'; import isEqual from 'lodash/isEqual'; -import PropTypes from 'prop-types'; import React, {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; import {useAsync, useToggle} from 'react-use'; @@ -11,10 +10,11 @@ import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; import {Checkbox} from 'components/admin/forms/Inputs'; import Select, {LOADING_OPTION} from 'components/admin/forms/Select'; +import {TargetPathSelect} from 'components/admin/forms/objects_api'; +import {TargetPathDisplay} from 'components/admin/forms/objects_api'; import ErrorMessage from 'components/errors/ErrorMessage'; import {post} from 'utils/fetch'; -import {TargetPathDisplay} from './ObjectsApiVariableConfigurationEditor'; import {asJsonSchema} from './utils'; export const GenericEditor = ({ @@ -137,51 +137,3 @@ export const GenericEditor = ({ ); }; - -const TargetPathSelect = ({name, index, choices, mappedVariable, disabled}) => { - // To avoid having an incomplete variable mapping added in the `variablesMapping` array, - // It is added only when an actual target path is selected. This way, having the empty - // option selected means the variable is unmapped (hence the `arrayHelpers.remove` call below). - const { - values: {variablesMapping}, - getFieldProps, - setFieldValue, - } = useFormikContext(); - const props = getFieldProps(name); - const isNew = variablesMapping.length === index; - - return ( - ( - { + if (event.target.value === '') { + arrayHelpers.remove(index); + } else { + if (isNew) { + arrayHelpers.push({...mappedVariable, targetPath: JSON.parse(event.target.value)}); + } else { + setFieldValue(name, JSON.parse(event.target.value)); + } + } + }} + /> + )} + /> + ); +}; + +TargetPathSelect.propTypes = { + name: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + choices: PropTypes.array.isRequired, + mappedVariable: PropTypes.object, +}; + +export default TargetPathSelect; diff --git a/src/openforms/js/components/admin/forms/objects_api/index.js b/src/openforms/js/components/admin/forms/objects_api/index.js index 01c8607560..4040eaf192 100644 --- a/src/openforms/js/components/admin/forms/objects_api/index.js +++ b/src/openforms/js/components/admin/forms/objects_api/index.js @@ -2,3 +2,5 @@ export {default as AuthAttributePath} from './AuthAttributePath'; export {default as ObjectsAPIGroup} from './ObjectsAPIGroup'; export {default as ObjectTypeSelect} from './ObjectTypeSelect'; export {default as ObjectTypeVersionSelect} from './ObjectTypeVersionSelect'; +export {default as TargetPathDisplay} from './TargetPathDisplay'; +export {default as TargetPathSelect} from './TargetPathSelect'; From 00e1d1688e5e2623707e29f19ea38c14d5e980d9 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 29 Nov 2024 09:42:15 +0100 Subject: [PATCH 23/39] :ok_hand: [#4398] Process PR feedback * raise PermissionDenied instead of ImproperlyConfigured in verify_initial_data_ownership in case of bad configuration * let PermissionDenied errors bubble up during prefill, causing a 403 * show missing validation errors for other objects API prefill fields --- .../variables/VariablesEditor.stories.js | 8 +++++++- .../prefill/objects_api/ObjectsAPIFields.js | 6 ++++++ .../admin/forms/objects_api/ObjectTypeSelect.js | 14 +++++++++++++- .../forms/objects_api/ObjectTypeVersionSelect.js | 7 ++++++- .../admin/forms/objects_api/ObjectsAPIGroup.js | 7 +++++++ .../prefill/contrib/objects_api/plugin.py | 4 ++-- .../test_initial_data_ownership_validation.py | 6 ++++-- src/openforms/prefill/sources.py | 2 +- .../prefill/tests/test_prefill_variables.py | 3 ++- .../registrations/contrib/objects_api/plugin.py | 4 ++-- .../test_initial_data_ownership_validation.py | 4 ++-- 11 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index ce23b7f178..fd51f5588c 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -874,7 +874,13 @@ export const ConfigurePrefillObjectsAPIWithValidationErrors = { ], }, errors: { - prefillOptions: {authAttributePath: 'This list may not be empty.'}, + prefillPlugin: 'Computer says no.', + prefillOptions: { + objectsApiGroup: 'Computer says no.', + objecttypeUuid: 'Computer says no.', + objecttypeVersion: 'Computer says no.', + authAttributePath: 'This list may not be empty.', + }, }, }, ], diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index b6c6a35d15..09cdf150a4 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -129,6 +129,9 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { if (error) throw error; const prefillProperties = loading ? LOADING_OPTION : value; + const [, objectsApiGroupErrors] = normalizeErrors(errors.options?.objectsApiGroup, intl); + const [, objecttypeUuidErrors] = normalizeErrors(errors.options?.objecttypeUuid, intl); + const [, objecttypeVersionErrors] = normalizeErrors(errors.options?.objecttypeVersion, intl); const [, authAttributePathErrors] = normalizeErrors(errors.options?.authAttributePath, intl); return ( @@ -142,6 +145,7 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => {
{ if (!objecttypeUuid) return true; const confirmSwitch = await openApiGroupConfirmationModal(); @@ -176,6 +180,7 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { name="options.objecttypeUuid" apiGroupFieldName="options.objectsApiGroup" versionFieldName="options.objecttypeVersion" + errors={objecttypeUuidErrors} label={ { defaultMessage="Version" /> } + errors={objecttypeVersionErrors} apiGroupFieldName="options.objectsApiGroup" objectTypeFieldName="options.objecttypeUuid" /> diff --git a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js index a7c19eee49..d4d8e9d050 100644 --- a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js +++ b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js @@ -26,6 +26,7 @@ const ObjectTypeSelect = ({ label, helpText, versionFieldName = 'objecttypeVersion', + errors = [], }) => { const [fieldProps, , fieldHelpers] = useField(name); const { @@ -68,7 +69,14 @@ const ObjectTypeSelect = ({ return ( - + { const {getFieldProps} = useFormikContext(); @@ -55,7 +56,7 @@ const ObjectTypeVersionSelect = ({ const options = choices.map(([value, label]) => ({value, label})); return ( - + { const [{onChange: onChangeFormik, ...fieldProps}, , {setValue}] = useField(name); const {setValues} = useFormikContext(); @@ -40,6 +41,7 @@ const ObjectsAPIGroup = ({ None: logevent.object_ownership_check_improperly_configured( submission, plugin=self ) - raise ImproperlyConfigured( + raise PermissionDenied( f"{backend} has no `auth_attribute_path` configured, cannot perform initial data ownership check" ) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py index bee9c1e967..729ecd4ebc 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.exceptions import PermissionDenied from django.test import TestCase, tag from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory @@ -138,7 +138,7 @@ def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failin with patch( "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", ) as mock_validate_object_ownership: - with self.assertRaises(ImproperlyConfigured): + with self.assertRaises(PermissionDenied): pre_registration(submission.id, PostSubmissionEvents.on_completion) # Not called, due to missing `auth_attribute_path` From 22b6430960ce0eb68dd096f956db2bf0051c7ac6 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 29 Nov 2024 10:16:33 +0100 Subject: [PATCH 24/39] :white_check_mark: [#4398] Re-record VCR cassettes for Objects API prefill --- ...nTests.test_prefill_values_happy_flow.yaml | 26 +++++----- ...efill_values_when_reference_not_found.yaml | 50 +------------------ ...s_when_reference_returns_empty_values.yaml | 20 ++++---- .../contrib/objects_api/tests/test_prefill.py | 5 +- 4 files changed, 28 insertions(+), 73 deletions(-) diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml index d69d558711..fb053cdd25 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_happy_flow.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", "record": {"typeVersion": 3, "data": {"name": {"last.name": "My last name"}, - "age": 45, "bsn": "111222333"}, "startAt": "2024-11-05"}}' + "age": 45, "bsn": "111222333"}, "startAt": "2024-11-29"}}' headers: Accept: - '*/*' @@ -24,8 +24,8 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My - last name"},"age":45,"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4","uuid":"2ec61245-a19a-4393-a953-fef7e3151de4","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"name":{"last.name":"My + last name"},"age":45,"bsn":"111222333"},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -40,9 +40,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:30 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Location: - - http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 + - http://localhost:8002/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4 Referrer-Policy: - same-origin Server: @@ -72,11 +72,11 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 + uri: http://localhost:8002/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My - last name"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4","uuid":"2ec61245-a19a-4393-a953-fef7e3151de4","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -91,7 +91,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:31 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Referrer-Policy: - same-origin Server: @@ -121,11 +121,11 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446 + uri: http://localhost:8002/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/34aeaf6a-aec0-4c56-bcb3-cf0173f77446","uuid":"34aeaf6a-aec0-4c56-bcb3-cf0173f77446","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My - last name"}},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/2ec61245-a19a-4393-a953-fef7e3151de4","uuid":"2ec61245-a19a-4393-a953-fef7e3151de4","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"age":45,"bsn":"111222333","name":{"last.name":"My + last name"}},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -140,7 +140,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:31 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml index d487947b3e..66891ed91d 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_not_found.yaml @@ -33,55 +33,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 15:01:56 GMT - Referrer-Policy: - - same-origin - Server: - - nginx/1.27.0 - Vary: - - origin - X-Content-Type-Options: - - nosniff - X-Frame-Options: - - DENY - status: - code: 404 - message: Not Found -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate, br - Authorization: - - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 - Connection: - - keep-alive - Content-Crs: - - EPSG:4326 - User-Agent: - - python-requests/2.32.2 - method: GET - uri: http://localhost:8002/api/v2/objects/048a37ca-a602-4158-9e60-9f06f3e47e2a - response: - body: - string: '{"detail":"Not found."}' - headers: - Allow: - - GET, PUT, PATCH, DELETE, HEAD, OPTIONS - Connection: - - keep-alive - Content-Crs: - - EPSG:4326 - Content-Length: - - '23' - Content-Type: - - application/json - Cross-Origin-Opener-Policy: - - same-origin - Date: - - Tue, 05 Nov 2024 15:01:56 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml index ff73d2e850..a38ebfe383 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillPluginTests/ObjectsAPIPrefillPluginTests.test_prefill_values_when_reference_returns_empty_values.yaml @@ -1,7 +1,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", - "record": {"typeVersion": 3, "data": {"bsn": "111222333"}, "startAt": "2024-11-05"}}' + "record": {"typeVersion": 3, "data": {"bsn": "111222333"}, "startAt": "2024-11-29"}}' headers: Accept: - '*/*' @@ -23,7 +23,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350","uuid":"da42d72d-f1eb-45e4-bb66-8bee69964350","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -38,9 +38,9 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:25 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Location: - - http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 + - http://localhost:8002/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350 Referrer-Policy: - same-origin Server: @@ -70,10 +70,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 + uri: http://localhost:8002/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350","uuid":"da42d72d-f1eb-45e4-bb66-8bee69964350","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -88,7 +88,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:25 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Referrer-Policy: - same-origin Server: @@ -118,10 +118,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145 + uri: http://localhost:8002/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350 response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/264ebc07-7cba-4ef4-8a3c-fbb802888145","uuid":"264ebc07-7cba-4ef4-8a3c-fbb802888145","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-05","endAt":null,"registrationAt":"2024-11-05","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/da42d72d-f1eb-45e4-bb66-8bee69964350","uuid":"da42d72d-f1eb-45e4-bb66-8bee69964350","type":"http://objecttypes-web:8000/api/v2/objecttypes/8e46e0a5-b1b4-449b-b9e9-fa3cea655f48","record":{"index":1,"typeVersion":3,"data":{"bsn":"111222333"},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -136,7 +136,7 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 05 Nov 2024 14:59:25 GMT + - Fri, 29 Nov 2024 09:14:40 GMT Referrer-Policy: - same-origin Server: diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py index 7ecaa8e3c8..81b909a012 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_prefill.py @@ -1,6 +1,8 @@ from pathlib import Path from unittest.mock import patch +from django.core.exceptions import PermissionDenied + from rest_framework.test import APITestCase from openforms.authentication.service import AuthAttribute @@ -138,7 +140,8 @@ def test_prefill_values_when_reference_not_found(self): }, ) - prefill_variables(submission=submission) + with self.assertRaises(PermissionDenied): + prefill_variables(submission=submission) state = submission.load_submission_value_variables_state() self.assertEqual(TimelineLogProxy.objects.count(), 1) From 1cd37e4850d6b0d161bbd68ded4c7ef975809ef2 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 29 Nov 2024 10:55:36 +0100 Subject: [PATCH 25/39] :white_check_mark: [#4398] Fix coverage for Objects API ownership validation --- .../objects_api/ownership_validation.py | 23 +- ...th_attribute_path_is_badly_configured.yaml | 202 ++++++++++++++++++ .../tests/test_ownership_validation.py | 84 +++++++- 3 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_ownership_check_fails_if_auth_attribute_path_is_badly_configured.yaml diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py index d518d513bc..3a692014d8 100644 --- a/src/openforms/contrib/objects_api/ownership_validation.py +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -24,7 +24,6 @@ def validate_object_ownership( client: ObjectsClient, object_attribute: list[str], plugin: BasePrefillPlugin | BaseRegistrationPlugin, - raise_exception: bool = True, ) -> None: """ Function to check whether the user associated with a Submission is the owner @@ -52,23 +51,19 @@ def validate_object_ownership( "Something went wrong while trying to retrieve " "object for initial_data_reference" ) - if raise_exception: - raise PermissionDenied from e + raise PermissionDenied from e - if not object: - # If the object cannot be found, we cannot consider the ownership check failed - # because it is not verified that the user is not the owner - logger.warning( - "Could not find object for initial_data_reference: %s", - submission.initial_data_reference, + if not object_attribute: + logger.exception( + "No path for auth value configured: %s, cannot perform ownership check", + object_attribute, + ) + raise PermissionDenied( + "Could not verify if user is owner of the referenced object" ) - if raise_exception: - raise PermissionDenied("Could not find object for initial_data_reference") - else: - return try: - auth_value = glom(object["record"]["data"], Path(*object_attribute)) + auth_value = glom(object["record"]["data"], Path(*(object_attribute or []))) except PathAccessError as e: logger.exception( "Could not retrieve auth value for path %s, it could be incorrectly configured", diff --git a/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_ownership_check_fails_if_auth_attribute_path_is_badly_configured.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_ownership_check_fails_if_auth_attribute_path_is_badly_configured.yaml new file mode 100644 index 0000000000..6dccd4b5ea --- /dev/null +++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_ownership_check_fails_if_auth_attribute_path_is_badly_configured.yaml @@ -0,0 +1,202 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"nested": {"bsn": "111222333"}, "foo": + "bar"}, "startAt": "2024-11-29"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '206' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c","uuid":"c614f674-04dc-435f-a801-99277148b69c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"nested":{"bsn":"111222333"},"foo":"bar"},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 29 Nov 2024 09:50:58 GMT + Location: + - http://localhost:8002/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c","uuid":"c614f674-04dc-435f-a801-99277148b69c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 29 Nov 2024 09:50:58 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c","uuid":"c614f674-04dc-435f-a801-99277148b69c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 29 Nov 2024 09:50:58 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c614f674-04dc-435f-a801-99277148b69c","uuid":"c614f674-04dc-435f-a801-99277148b69c","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"foo":"bar","nested":{"bsn":"111222333"}},"geometry":null,"startAt":"2024-11-29","endAt":null,"registrationAt":"2024-11-29","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '433' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Fri, 29 Nov 2024 09:50:58 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.0 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 3f4ca918d8..4ed0f4dd51 100644 --- a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied from django.test import TestCase, override_settings, tag -from requests.exceptions import RequestException +from requests.exceptions import HTTPError, RequestException from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client @@ -176,6 +176,56 @@ def test_user_is_not_owner_of_object_nested_auth_attribute(self): str(cm.exception), "User is not the owner of the referenced object" ) + @tag("gh-4398") + def test_ownership_check_fails_if_auth_attribute_path_is_badly_configured(self): + with get_objects_client(self.objects_api_group_used) as client: + object = client.create_object( + record_data=prepare_data_for_registration( + data={"nested": {"bsn": "111222333"}, "foo": "bar"}, + objecttype_version=1, + ), + objecttype_url="http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + ) + object_ref = object["uuid"] + + submission = SubmissionFactory.create( + auth_info__value="123456782", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=object_ref, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.subTest("empty path"): + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership(submission, client, [], PLUGIN) + self.assertEqual( + str(cm.exception), + "Could not verify if user is owner of the referenced object", + ) + + with self.subTest("non existent path"): + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied) as cm: + validate_object_ownership( + submission, client, ["this", "does", "not", "exist"], PLUGIN + ) + self.assertEqual( + str(cm.exception), + "Could not verify if user is owner of the referenced object", + ) + @tag("gh-4398") @patch( "openforms.contrib.objects_api.clients.objects.ObjectsClient.get_object", @@ -208,6 +258,38 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): with get_objects_client(self.objects_api_group_used) as client: validate_object_ownership(submission, client, ["bsn"], PLUGIN) + @tag("gh-4398") + @patch( + "openforms.contrib.objects_api.clients.objects.ObjectsClient.get_object", + side_effect=HTTPError("404"), + ) + def test_object_not_found_when_doing_permission_check(self, mock_get_object): + """ + If the object could not be fetched due to request errors, the ownership check + should fail + """ + submission = SubmissionFactory.create( + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) + + # The backend that should be used to perform the check + FormRegistrationBackendFactory.create( + form=submission.form, + backend="objects_api", + options={ + "version": 2, + "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", + "objects_api_group": self.objects_api_group_used.pk, + "objecttype_version": 1, + }, + ) + + with self.assertRaises(PermissionDenied): + with get_objects_client(self.objects_api_group_used) as client: + validate_object_ownership(submission, client, ["bsn"], PLUGIN) + @tag("gh-4398") def test_no_backends_configured_does_not_raise_error( self, From addb139feb89ea4794096164be205c552fe4618a Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 2 Dec 2024 13:42:02 +0100 Subject: [PATCH 26/39] :globe_with_meridians: [#4398] Update extracted translations (JS) --- src/openforms/js/compiled-lang/en.json | 6 ++++++ src/openforms/js/compiled-lang/nl.json | 6 ++++++ src/openforms/js/lang/en.json | 5 +++++ src/openforms/js/lang/nl.json | 5 +++++ 4 files changed, 22 insertions(+) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 2426de61dd..35e9958410 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -1613,6 +1613,12 @@ "value": "Street name" } ], + "DFQ0Pq": [ + { + "type": 0, + "value": "Update existing objects" + } + ], "DGpAyT": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 2728037ed2..2c2060d55b 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -1634,6 +1634,12 @@ "value": "Straatnaam" } ], + "DFQ0Pq": [ + { + "type": 0, + "value": "Update existing objects" + } + ], "DGpAyT": [ { "type": 0, diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json index 9b8e40ce86..8eea60e6ca 100644 --- a/src/openforms/js/lang/en.json +++ b/src/openforms/js/lang/en.json @@ -699,6 +699,11 @@ "description": "JSON variable type \"boolean\" representation", "originalDefault": "Boolean" }, + "DFQ0Pq": { + "defaultMessage": "Update existing objects", + "description": "Objects registration: update existing objects settings", + "originalDefault": "Update existing objects" + }, "DGpAyT": { "defaultMessage": "Move down", "description": "Move down icon title", diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json index 06b9f74fe8..032d9496bd 100644 --- a/src/openforms/js/lang/nl.json +++ b/src/openforms/js/lang/nl.json @@ -705,6 +705,11 @@ "description": "JSON variable type \"boolean\" representation", "originalDefault": "Boolean" }, + "DFQ0Pq": { + "defaultMessage": "Update existing objects", + "description": "Objects registration: update existing objects settings", + "originalDefault": "Update existing objects" + }, "DGpAyT": { "defaultMessage": "Verplaats omlaag", "description": "Move down icon title", From 215fa49246b99e9397d190c47cae68dde9ec7873 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 2 Dec 2024 15:03:37 +0100 Subject: [PATCH 27/39] :recycle: [#4398] Refactor the TargetPathSelect component The TargetPathSelect component is now decoupled from its possible FieldArray parent, and the option label display is incorporated in the component itself so that options no longer need to be pre-processed. On top of that, it's now refactored to be based on react-select for consistency in the UI, and separate stories have been added so we can do (visual) regression testing/isolated development. We use this component in a number of places now: * the path to the auth attribute (no variablesMapping parent context), used in registration and prefill plugins for the Objects API * the path for the generic Object Types V2 registration mapping, here a variablesMapping parent container/context is relevant and different form state management semantics apply * the path to specialized AddressNL subfield mappings - a parent is relevant, but not within a bigger variable mapping so we can use the standard field assignment semantics of Formik. --- src/openforms/js/compiled-lang/en.json | 6 + src/openforms/js/compiled-lang/nl.json | 6 + .../form_design/RegistrationFields.stories.js | 10 + ...NlObjectsApiVariableConfigurationEditor.js | 185 ++++++------------ ...icObjectsApiVariableConfigurationEditor.js | 79 ++++++-- .../objectsapi/LegacyConfigFields.js | 15 +- .../ObjectsApiOptionsFormFields.stories.js | 10 + .../ObjectsApiVariableConfigurationEditor.js | 3 +- .../objectsapi/V2ConfigFields.js | 16 +- .../variables/VariablesEditor.stories.js | 154 ++++++++------- .../forms/objects_api/AuthAttributePath.js | 31 +-- .../forms/objects_api/TargetPathDisplay.js | 23 --- .../forms/objects_api/TargetPathSelect.js | 107 ++++++---- .../objects_api/TargetPathSelect.stories.js | 43 ++++ .../admin/forms/objects_api/index.js | 3 +- src/openforms/js/lang/en.json | 5 + src/openforms/js/lang/nl.json | 5 + 17 files changed, 408 insertions(+), 293 deletions(-) delete mode 100644 src/openforms/js/components/admin/forms/objects_api/TargetPathDisplay.js create mode 100644 src/openforms/js/components/admin/forms/objects_api/TargetPathSelect.stories.js diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 35e9958410..790c4a6d3e 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -6427,6 +6427,12 @@ "value": "Variable" } ], + "xBb5YI": [ + { + "type": 0, + "value": "Select an object type and version before you can pick a source path." + } + ], "xI6md8": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 2c2060d55b..e354b6c13e 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -6449,6 +6449,12 @@ "value": "Variabele" } ], + "xBb5YI": [ + { + "type": 0, + "value": "Select an object type and version before you can pick a source path." + } + ], "xI6md8": [ { "type": 0, diff --git a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js index 0d28050491..726b9cc2fa 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -6,6 +6,7 @@ import { mockCataloguesGet as mockObjectsApiCataloguesGet, mockObjecttypeVersionsGet, mockObjecttypesGet, + mockTargetPathsPost, } from 'components/admin/form_design/registrations/objectsapi/mocks'; import { mockCaseTypesGet, @@ -512,6 +513,15 @@ export default { ]), mockObjectsApiCataloguesGet(), mockDocumentTypesGet(), + mockTargetPathsPost({ + string: [ + { + targetPath: ['path', 'to.the', 'target'], + isRequired: true, + jsonSchema: {type: 'string'}, + }, + ], + }), ], zgwMocks: [ mockZGWApisCataloguesGet(), diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js index 7f9265580b..f6b5f624c4 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js @@ -1,6 +1,5 @@ -import {FieldArray, useFormikContext} from 'formik'; +import {useFormikContext} from 'formik'; import isEqual from 'lodash/isEqual'; -import PropTypes from 'prop-types'; import React, {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; import {useAsync, useToggle} from 'react-use'; @@ -11,11 +10,12 @@ import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {Checkbox} from 'components/admin/forms/Inputs'; -import Select, {LOADING_OPTION} from 'components/admin/forms/Select'; -import {TargetPathDisplay} from 'components/admin/forms/objects_api'; +import {TargetPathSelect} from 'components/admin/forms/objects_api'; import ErrorMessage from 'components/errors/ErrorMessage'; import {post} from 'utils/fetch'; +import {MappedVariableTargetPathSelect} from './GenericObjectsApiVariableConfigurationEditor'; + const ADDRESSNL_NESTED_PROPERTIES = { postcode: {type: 'string'}, houseLetter: {type: 'string'}, @@ -58,16 +58,17 @@ export const AddressNlEditor = ({ objecttypeVersion, }) => { const {csrftoken} = useContext(APIContext); - const {values, setFieldValue} = useFormikContext(); + const {setValues} = useFormikContext(); + + const hasSpecificOptions = Object.values(mappedVariable?.options ?? {}).some( + targetPath => targetPath && targetPath.length + ); + const [specificTargetPaths, toggleSpecificTargetPaths] = useToggle(hasSpecificOptions); const [jsonSchemaVisible, toggleJsonSchemaVisible] = useToggle(false); - const {specificTargetPaths} = values; - const isSpecificTargetPaths = - specificTargetPaths || - (mappedVariable.options && Object.keys(mappedVariable.options).length > 0); const deriveAddress = components[variable?.key]['deriveAddress']; - // // Load all the possible target paths (obect,string and number types) in parallel and only once + // Load all the possible target paths (obect,string and number types) in parallel and only once const { loading, value: targetPaths, @@ -90,20 +91,6 @@ export const AddressNlEditor = ({ const [objectTypeTargetPaths = [], stringTypeTargetPaths = [], numberTypeTargetPaths = []] = targetPaths || []; - const choicesTypes = { - object: objectTypeTargetPaths, - string: stringTypeTargetPaths, - number: numberTypeTargetPaths, - }; - - const getChoices = type => - loading || error - ? LOADING_OPTION - : choicesTypes[type].map(t => [ - JSON.stringify(t.targetPath), - , - ]); - const getTargetPath = pathSegment => objectTypeTargetPaths.find(t => isEqual(t.targetPath, pathSegment)); @@ -118,18 +105,31 @@ export const AddressNlEditor = ({ ); const onSpecificTargetPathsChange = event => { - setFieldValue('specificTargetPaths', event.target.checked); + const makeSpecific = event.target.checked; + toggleSpecificTargetPaths(makeSpecific); - if (event.target.checked) { - setFieldValue(`${namePrefix}.targetPath`, undefined); - } else { - setFieldValue(`${namePrefix}.options.postcode`, undefined); - setFieldValue(`${namePrefix}.options.houseLetter`, undefined); - setFieldValue(`${namePrefix}.options.houseNumber`, undefined); - setFieldValue(`${namePrefix}.options.houseNumberAddition`, undefined); - setFieldValue(`${namePrefix}.options.city`, undefined); - setFieldValue(`${namePrefix}.options.streetName`, undefined); - } + setValues(prevValues => { + const newVariablesMapping = [...prevValues.variablesMapping]; + const newMappedVariable = { + ...(newVariablesMapping[index] ?? mappedVariable), + // clear targetPath if we're switching to specific subfields + targetPath: makeSpecific ? undefined : mappedVariable.targetPath, + // prepare the options structure if we're switching to specific subfields, + // otherwise remove it entirely + options: makeSpecific + ? { + postcode: undefined, + houseLetter: undefined, + houseNumber: undefined, + houseNumberAddition: undefined, + city: undefined, + streetName: undefined, + } + : undefined, + }; + newVariablesMapping[index] = newMappedVariable; + return {...prevValues, variablesMapping: newVariablesMapping}; + }); }; return ( @@ -150,7 +150,7 @@ export const AddressNlEditor = ({ defaultMessage="Whether to map the specific subfield of addressNl component" /> } - checked={isSpecificTargetPaths} + checked={specificTargetPaths} onChange={onSpecificTargetPathsChange} /> @@ -164,18 +164,19 @@ export const AddressNlEditor = ({ description="'JSON Schema object target' label" /> } - disabled={isSpecificTargetPaths} + disabled={specificTargetPaths} > - - {isSpecificTargetPaths && ( + {specificTargetPaths && (
} required + noManageChildProps > @@ -207,13 +207,12 @@ export const AddressNlEditor = ({ /> } required + noManageChildProps > @@ -226,13 +225,12 @@ export const AddressNlEditor = ({ description="'Objects registration variable mapping, addressNL component: 'options.houseLetter schema target' label" /> } + noManageChildProps > @@ -245,13 +243,12 @@ export const AddressNlEditor = ({ description="Objects registration variable mapping, addressNL component: 'options.houseNumberAddition schema target' label" /> } + noManageChildProps > @@ -265,14 +262,13 @@ export const AddressNlEditor = ({ /> } disabled={!deriveAddress} + noManageChildProps > @@ -286,20 +282,19 @@ export const AddressNlEditor = ({ /> } disabled={!deriveAddress} + noManageChildProps >
)} - {!isSpecificTargetPaths && ( + {!specificTargetPaths && (
e.preventDefault() || toggleJsonSchemaVisible()}> ); }; - -const TargetPathSelect = ({id, name, index, choices, mappedVariable, disabled}) => { - // To avoid having an incomplete variable mapping added in the `variablesMapping` array, - // It is added only when an actual target path is selected. This way, having the empty - // option selected means the variable is unmapped (hence the `arrayHelpers.remove` call below). - const { - values: {variablesMapping}, - getFieldProps, - setFieldValue, - } = useFormikContext(); - const props = getFieldProps(name); - const isNew = variablesMapping.length === index; - - return ( - ( - { - if (event.target.value === '') { - arrayHelpers.remove(index); - } else { - if (isNew) { - arrayHelpers.push({...mappedVariable, targetPath: JSON.parse(event.target.value)}); - } else { - setFieldValue(name, JSON.parse(event.target.value)); - } - } - }} - /> - )} + o.value === JSON.stringify(value)) : null} + components={{ + Option: props => ( + + + + ), + }} + onChange={newValue => { + setFieldValue(name, newValue ? newValue.targetPath : undefined); + }} + {...props} /> ); }; TargetPathSelect.propTypes = { name: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - choices: PropTypes.array.isRequired, - mappedVariable: PropTypes.object, + isLoading: PropTypes.bool, + targetPaths: PropTypes.arrayOf(TargetPathType), + isDisabled: PropTypes.bool, }; export default TargetPathSelect; diff --git a/src/openforms/js/components/admin/forms/objects_api/TargetPathSelect.stories.js b/src/openforms/js/components/admin/forms/objects_api/TargetPathSelect.stories.js new file mode 100644 index 0000000000..e7106c0c55 --- /dev/null +++ b/src/openforms/js/components/admin/forms/objects_api/TargetPathSelect.stories.js @@ -0,0 +1,43 @@ +import {FormikDecorator} from 'components/admin/form_design/story-decorators'; + +import TargetPathSelect from './TargetPathSelect'; + +export default { + title: 'Form design / TargetPathSelect', + component: TargetPathSelect, + decorators: [FormikDecorator], + args: { + name: 'someTarget', + isLoading: false, + targetPaths: [ + { + targetPath: ['root', 'child'], + isRequired: false, + }, + { + targetPath: ['root', 'other child'], + isRequired: true, + }, + ], + isDisabled: false, + }, + parameters: { + formik: { + initialValues: { + someTarget: null, + }, + }, + }, +}; + +export const Default = {}; + +export const ValueSelected = { + parameters: { + formik: { + initialValues: { + someTarget: ['root', 'other child'], + }, + }, + }, +}; diff --git a/src/openforms/js/components/admin/forms/objects_api/index.js b/src/openforms/js/components/admin/forms/objects_api/index.js index 4040eaf192..a1ace2bede 100644 --- a/src/openforms/js/components/admin/forms/objects_api/index.js +++ b/src/openforms/js/components/admin/forms/objects_api/index.js @@ -2,5 +2,4 @@ export {default as AuthAttributePath} from './AuthAttributePath'; export {default as ObjectsAPIGroup} from './ObjectsAPIGroup'; export {default as ObjectTypeSelect} from './ObjectTypeSelect'; export {default as ObjectTypeVersionSelect} from './ObjectTypeVersionSelect'; -export {default as TargetPathDisplay} from './TargetPathDisplay'; -export {default as TargetPathSelect} from './TargetPathSelect'; +export {default as TargetPathSelect, TargetPathDisplay} from './TargetPathSelect'; diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json index 8eea60e6ca..9b023b43d0 100644 --- a/src/openforms/js/lang/en.json +++ b/src/openforms/js/lang/en.json @@ -3029,6 +3029,11 @@ "description": "Price variable label", "originalDefault": "Variable" }, + "xBb5YI": { + "defaultMessage": "Select an object type and version before you can pick a source path.", + "description": "Object type target path selection for auth attribute message for missing options because no object type has been selected.", + "originalDefault": "Select an object type and version before you can pick a source path." + }, "xJBMaf": { "defaultMessage": "Submission confirmation template", "description": "Submission confirmation fieldset title", diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json index 032d9496bd..14da689daa 100644 --- a/src/openforms/js/lang/nl.json +++ b/src/openforms/js/lang/nl.json @@ -3050,6 +3050,11 @@ "description": "Price variable label", "originalDefault": "Variable" }, + "xBb5YI": { + "defaultMessage": "Select an object type and version before you can pick a source path.", + "description": "Object type target path selection for auth attribute message for missing options because no object type has been selected.", + "originalDefault": "Select an object type and version before you can pick a source path." + }, "xJBMaf": { "defaultMessage": "Sjabloon bevestigingspagina", "description": "Submission confirmation fieldset title", From eec2eb3b54008c42dca0e633a5f248c3c8dd4b5c Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 3 Dec 2024 09:58:27 +0100 Subject: [PATCH 28/39] :art: Use canvas instead of screen for DOM queries This is now possible because we render modals in the proper portal/body element to make these queries work. --- .../variables/VariablesEditor.stories.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index e344bf11c2..b46397c896 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -1,4 +1,4 @@ -import {expect, fn, screen, userEvent, waitFor, within} from '@storybook/test'; +import {expect, fn, userEvent, waitFor, within} from '@storybook/test'; import selectEvent from 'react-select-event'; import { @@ -587,7 +587,7 @@ export const ConfigurePrefill = { const editIcon = canvas.getByTitle('Prefill instellen'); await userEvent.click(editIcon); - const pluginDropdown = await screen.findByLabelText('Plugin'); + const pluginDropdown = await canvas.findByLabelText('Plugin'); expect(pluginDropdown).toBeVisible(); expect(await within(pluginDropdown).findByRole('option', {name: 'StUF-BG'})).toBeVisible(); }, @@ -632,18 +632,18 @@ export const ConfigurePrefillObjectsAPI = { await step('Configure Objects API prefill', async () => { const modal = within(await canvas.findByRole('dialog')); - const pluginDropdown = await screen.findByLabelText('Plugin'); + const pluginDropdown = await canvas.findByLabelText('Plugin'); expect(pluginDropdown).toBeVisible(); await userEvent.selectOptions(pluginDropdown, 'Objects API'); // check mappings - const variableSelect = await screen.findByLabelText('Formuliervariabele'); + const variableSelect = await canvas.findByLabelText('Formuliervariabele'); expect(variableSelect).toBeVisible(); expect(modal.getByText('Form.io component')).toBeVisible(); // Wait until the API call to retrieve the prefillAttributes is done await waitFor(async () => { - const prefillPropertySelect = await screen.findByLabelText( + const prefillPropertySelect = await canvas.findByLabelText( 'Selecteer een attribuut uit het objecttype' ); expect(prefillPropertySelect).toBeVisible(); @@ -730,12 +730,12 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { // open modal for configuration const editIcon = canvas.getByTitle('Prefill instellen'); await userEvent.click(editIcon); - expect(await screen.findByRole('dialog')).toBeVisible(); + expect(await canvas.findByRole('dialog')).toBeVisible(); }); await step('Configure Objects API prefill with copy button', async () => { - const modal = within(await screen.findByRole('dialog')); - const pluginDropdown = await screen.findByLabelText('Plugin'); + const modal = within(await canvas.findByRole('dialog')); + const pluginDropdown = await canvas.findByLabelText('Plugin'); expect(pluginDropdown).toBeVisible(); await userEvent.selectOptions(pluginDropdown, 'Objects API'); @@ -765,7 +765,7 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { expect(button).toBeVisible(); await userEvent.click(button); - const modalForm = await screen.findByTestId('modal-form'); + const modalForm = await canvas.findByTestId('modal-form'); expect(modalForm).toBeVisible(); const propertyDropdowns = await modal.findAllByLabelText( 'Selecteer een attribuut uit het objecttype' From c77276c49005cc60e2f58c05aa79ab399d85faff Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 3 Dec 2024 10:49:08 +0100 Subject: [PATCH 29/39] :children_crossing: [#4398] Disable copy button until a backend is selected --- .../CopyConfigurationFromRegistrationBackend.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js index b4d4d44c25..156a0a40a3 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js @@ -49,7 +49,6 @@ const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) setFieldValue(name, selectedOption.value); }} maxMenuHeight="16em" - menuPlacement="bottom" />
- ); -}; + +); PrefillConfigurationForm.propTypes = { onSubmit: PropTypes.func.isRequired, @@ -107,6 +121,7 @@ PrefillConfigurationForm.propTypes = { plugin: PropTypes.arrayOf(PropTypes.string), attribute: PropTypes.arrayOf(PropTypes.string), identifierRole: PropTypes.arrayOf(PropTypes.string), + options: PropTypes.object, }).isRequired, }; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js index 0d1c80d5dc..4efaf73818 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js @@ -29,7 +29,7 @@ const getAttributes = async plugin => { * Default (legacy) prefill configuration - after selecting the plugin, the user * selects which attribute to use to grab the prefill value from. */ -const DefaultFields = ({errors}) => { +const DefaultFields = () => { const { values: {plugin = ''}, } = useFormikContext(); @@ -60,7 +60,6 @@ const DefaultFields = ({errors}) => { defaultMessage="Attribute" /> } - errors={errors.attribute} > @@ -75,7 +74,6 @@ const DefaultFields = ({errors}) => { defaultMessage="Identifier role" /> } - errors={errors.identifierRole} > @@ -84,12 +82,6 @@ const DefaultFields = ({errors}) => { ); }; -DefaultFields.propTypes = { - errors: PropTypes.shape({ - plugin: ErrorsType, - attribute: ErrorsType, - identifierRole: ErrorsType, - }).isRequired, -}; +DefaultFields.propTypes = {}; export default DefaultFields; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index 09cdf150a4..141060eb59 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -11,10 +11,10 @@ import useAsync from 'react-use/esm/useAsync'; import {FormContext} from 'components/admin/form_design/Context'; import useConfirm from 'components/admin/form_design/useConfirm'; -import {normalizeErrors} from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {LOADING_OPTION} from 'components/admin/forms/Select'; +import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; import VariableMapping from 'components/admin/forms/VariableMapping'; import { AuthAttributePath, @@ -26,7 +26,7 @@ import {FAIcon} from 'components/admin/icons'; import ErrorBoundary from 'components/errors/ErrorBoundary'; import {get} from 'utils/fetch'; -import {ErrorsType} from '../types'; +import ValidationErrorsProvider from '../../../../forms/ValidationErrors'; import CopyConfigurationFromRegistrationBackend from './CopyConfigurationFromRegistrationBackend'; const PLUGIN_ID = 'objects_api'; @@ -40,7 +40,7 @@ const onApiGroupChange = prevValues => ({ ...prevValues.options, objecttypeUuid: '', objecttypeVersion: undefined, - authAttributePath: [], + authAttributePath: undefined, variablesMapping: [], }, }); @@ -56,40 +56,34 @@ const getProperties = async (objectsApiGroup, objecttypeUuid, objecttypeVersion) return response.data.map(property => [property.targetPath, property.targetPath.join(' > ')]); }; -const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { +const ObjectsAPIFields = ({showCopyButton, setShowCopyButton}) => { const intl = useIntl(); + // Object with keys the plugin/attribute/options, we process these further to set up + // the required context for the fields. + const errors = Object.fromEntries(useContext(ValidationErrorContext)); + const optionsErrors = Object.entries(errors.options ?? {}).map(([key, errs]) => [ + `options.${key}`, + errs, + ]); + const {values, setFieldValue, setValues} = useFormikContext(); const { - values, - values: { - plugin, - options: { - objecttypeUuid, - objecttypeVersion, - objectsApiGroup, - authAttributePath, - variablesMapping, - }, - }, - setFieldValue, - setValues, - } = useFormikContext(); + plugin, + options: {objecttypeUuid, objecttypeVersion, objectsApiGroup}, + } = values; const defaults = { objectsApiGroup: null, objecttypeUuid: '', objecttypeVersion: null, - authAttributePath: [], + authAttributePath: undefined, variablesMapping: [], }; // Merge defaults into options if not already set useEffect(() => { - if (!values.options) { - setFieldValue('options', defaults); - } else { - setFieldValue('options', {...defaults, ...values.options}); - } + const options = values.options ?? {}; + setFieldValue('options', {...defaults, ...options}); }, []); const { @@ -129,13 +123,8 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { if (error) throw error; const prefillProperties = loading ? LOADING_OPTION : value; - const [, objectsApiGroupErrors] = normalizeErrors(errors.options?.objectsApiGroup, intl); - const [, objecttypeUuidErrors] = normalizeErrors(errors.options?.objecttypeUuid, intl); - const [, objecttypeVersionErrors] = normalizeErrors(errors.options?.objecttypeVersion, intl); - const [, authAttributePathErrors] = normalizeErrors(errors.options?.authAttributePath, intl); - return ( - <> + {showCopyButton ? ( {
{ if (!objecttypeUuid) return true; const confirmSwitch = await openApiGroupConfirmationModal(); @@ -180,7 +168,6 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { name="options.objecttypeUuid" apiGroupFieldName="options.objectsApiGroup" versionFieldName="options.objecttypeVersion" - errors={objecttypeUuidErrors} label={ { defaultMessage="Version" /> } - errors={objecttypeVersionErrors} apiGroupFieldName="options.objectsApiGroup" objectTypeFieldName="options.objecttypeUuid" /> @@ -241,7 +227,6 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { objecttypeUuid={objecttypeUuid} objecttypeVersion={objecttypeVersion} style={{maxWidth: '10em'}} - errors={authAttributePathErrors} />
@@ -294,14 +279,13 @@ const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { /> } /> - +
); }; ObjectsAPIFields.propTypes = { - errors: PropTypes.shape({ - plugin: ErrorsType, - }).isRequired, + showCopyButton: PropTypes.bool.isRequired, + setShowCopyButton: PropTypes.func.isRequired, }; export default ObjectsAPIFields; diff --git a/src/openforms/js/components/admin/forms/Field.js b/src/openforms/js/components/admin/forms/Field.js index 23e99583fd..08f1c0884a 100644 --- a/src/openforms/js/components/admin/forms/Field.js +++ b/src/openforms/js/components/admin/forms/Field.js @@ -6,6 +6,28 @@ import {useIntl} from 'react-intl'; import {PrefixContext} from './Context'; import ErrorList from './ErrorList'; +/** + * @typedef {Object} IntlErrorMessage + * @property {string} defaultMessage + * @property {string} description + */ + +/** + * @typedef {[string, string]} NamedErrorMessage + * @property {string} 0 - The form field name with the error. + * @property {string} 1 - The error message itself. + */ + +/** + * @typedef {string | IntlErrorMessage | NamedErrorMessage} ErrorMessage + */ + +/** + * + * @param {ErrorMessage | ErrorMessage[]} errors A single error instance or array of errors. + * @param {IntlShape} intl The intl object from react-intl (useIntl() hook return value). + * @return {[boolean, string[]]} A tuple indicating if there are errors and the list of error messages. + */ export const normalizeErrors = (errors = [], intl) => { if (!Array.isArray(errors)) { errors = [errors]; diff --git a/src/openforms/js/components/admin/forms/ValidationErrors.js b/src/openforms/js/components/admin/forms/ValidationErrors.js index 1df7e12832..5b316c29c2 100644 --- a/src/openforms/js/components/admin/forms/ValidationErrors.js +++ b/src/openforms/js/components/admin/forms/ValidationErrors.js @@ -26,6 +26,12 @@ ValidationErrorsProvider.propTypes = { errors: PropTypes.arrayOf(PropTypes.arrayOf(errorArray)), }; +/** + * Only return the errors that have the $name prefix. + * @param {string} name Field name prefix + * @param {Array<[string, T>} errors List of all validation errors. + * @return {Array<[string, T>} List of errors that match the name prefix. + */ const filterErrors = (name, errors) => { return errors .filter(([key]) => key.startsWith(`${name}.`)) diff --git a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js index d4d8e9d050..a7c19eee49 100644 --- a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js +++ b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js @@ -26,7 +26,6 @@ const ObjectTypeSelect = ({ label, helpText, versionFieldName = 'objecttypeVersion', - errors = [], }) => { const [fieldProps, , fieldHelpers] = useField(name); const { @@ -69,14 +68,7 @@ const ObjectTypeSelect = ({ return ( - + { const {getFieldProps} = useFormikContext(); @@ -56,7 +55,7 @@ const ObjectTypeVersionSelect = ({ const options = choices.map(([value, label]) => ({value, label})); return ( - + { const [{onChange: onChangeFormik, ...fieldProps}, , {setValue}] = useField(name); const {setValues} = useFormikContext(); @@ -41,7 +40,6 @@ const ObjectsAPIGroup = ({ Date: Tue, 3 Dec 2024 14:12:00 +0100 Subject: [PATCH 31/39] :art: Apply fallback for missing form values --- src/openforms/js/components/admin/forms/VariableMapping.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openforms/js/components/admin/forms/VariableMapping.js b/src/openforms/js/components/admin/forms/VariableMapping.js index bab5846fe1..61bb4e8760 100644 --- a/src/openforms/js/components/admin/forms/VariableMapping.js +++ b/src/openforms/js/components/admin/forms/VariableMapping.js @@ -254,8 +254,8 @@ const VariableMapping = ({ onClick={() => { // TODO update const initial = {[variableName]: '', [propertyName]: ''}; - const mapping = get(values, name); - arrayHelpers.insert(mapping ? mapping.length : 0, initial); + const mapping = get(values, name) || []; + arrayHelpers.insert(mapping.length, initial); }} > From 6d45ca2fffac6298d44e2429fdcc34420f00797e Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 3 Dec 2024 15:18:51 +0100 Subject: [PATCH 32/39] :recycle: [#4398] Clean up ownership validation in prefill plugins * Ensure we use the deserialized, strongly typed options in plugins * Dropped unused error cases/flows that are obsoleted * Made serializer options all required, since the prefill cannot possibly work without and must match the type definitions of the options. --- .../objects_api/ownership_validation.py | 11 +- src/openforms/logging/models.py | 6 + src/openforms/prefill/base.py | 5 +- .../contrib/objects_api/api/serializers.py | 18 +- .../prefill/contrib/objects_api/plugin.py | 33 +-- ...d_if_initial_data_reference_specified.yaml | 56 ++++- ...raising_errors_causes_prefill_to_fail.yaml | 50 ++++ .../setUpTestData.yaml | 10 +- .../test_initial_data_ownership_validation.py | 230 +++++++----------- .../prefill/contrib/objects_api/typing.py | 3 +- src/openforms/prefill/sources.py | 48 ++-- 11 files changed, 261 insertions(+), 209 deletions(-) create mode 100644 src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail.yaml diff --git a/src/openforms/contrib/objects_api/ownership_validation.py b/src/openforms/contrib/objects_api/ownership_validation.py index 3a692014d8..ce7269b618 100644 --- a/src/openforms/contrib/objects_api/ownership_validation.py +++ b/src/openforms/contrib/objects_api/ownership_validation.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from django.core.exceptions import PermissionDenied @@ -10,14 +9,12 @@ from openforms.contrib.objects_api.clients import ObjectsClient from openforms.logging import logevent +from openforms.prefill.base import BasePlugin as BasePrefillPlugin +from openforms.registrations.base import BasePlugin as BaseRegistrationPlugin +from openforms.submissions.models import Submission logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from openforms.prefill.base import BasePlugin as BasePrefillPlugin - from openforms.registrations.base import BasePlugin as BaseRegistrationPlugin - from openforms.submissions.models import Submission - def validate_object_ownership( submission: Submission, @@ -63,7 +60,7 @@ def validate_object_ownership( ) try: - auth_value = glom(object["record"]["data"], Path(*(object_attribute or []))) + auth_value = glom(object["record"]["data"], Path(*object_attribute)) except PathAccessError as e: logger.exception( "Could not retrieve auth value for path %s, it could be incorrectly configured", diff --git a/src/openforms/logging/models.py b/src/openforms/logging/models.py index 54c9e33e50..41b6c1a149 100644 --- a/src/openforms/logging/models.py +++ b/src/openforms/logging/models.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.db import models from django.template.defaultfilters import capfirst from django.urls import reverse @@ -13,6 +14,11 @@ class TimelineLogProxyQueryset(models.QuerySet): + # vendored from https://github.com/maykinmedia/django-timeline-logger/pull/32/files + def for_object(self, obj: models.Model): + content_type = ContentType.objects.get_for_model(obj) + return self.filter(content_type=content_type, object_id=obj.pk) + def filter_event(self, event: str): return self.filter(extra_data__log_event=event) diff --git a/src/openforms/prefill/base.py b/src/openforms/prefill/base.py index 80ac16e049..81b6b61292 100644 --- a/src/openforms/prefill/base.py +++ b/src/openforms/prefill/base.py @@ -66,7 +66,7 @@ def get_prefill_values( raise NotImplementedError("You must implement the 'get_prefill_values' method.") def verify_initial_data_ownership( - self, submission: Submission, prefill_options: dict + self, submission: Submission, prefill_options: OptionsT ) -> None: """ Hook to check if the authenticated user is the owner of the object @@ -75,7 +75,8 @@ def verify_initial_data_ownership( If any error occurs in this check, it should raise a `PermissionDenied` :param submission: an active :class:`Submission` instance - :param prefill_options: a dictionary containing the configuration options + :param prefill_options: the configuration options, after validation and + deserialization through the :attr:`options` serializer class. """ raise NotImplementedError( "You must implement the 'verify_initial_data_ownership' method." diff --git a/src/openforms/prefill/contrib/objects_api/api/serializers.py b/src/openforms/prefill/contrib/objects_api/api/serializers.py index 91dd04a5c3..5470fdb307 100644 --- a/src/openforms/prefill/contrib/objects_api/api/serializers.py +++ b/src/openforms/prefill/contrib/objects_api/api/serializers.py @@ -24,12 +24,15 @@ class PrefillTargetPathsSerializer(serializers.Serializer): class ObjecttypeVariableMappingSerializer(serializers.Serializer): - """A mapping between a form variable key and the corresponding Objecttype attribute.""" + """ + A mapping between a form variable key and the corresponding Objecttype attribute. + """ variable_key = FormioVariableKeyField( label=_("variable key"), help_text=_( - "The 'dotted' path to a form variable key. The format should comply to how Formio handles nested component keys." + "The 'dotted' path to a form variable key. The format should comply to how " + "Formio handles nested component keys." ), ) target_path = serializers.ListField( @@ -47,24 +50,25 @@ class ObjectsAPIOptionsSerializer(JsonSchemaSerializerMixin, serializers.Seriali Q(objects_service=None) | Q(objecttypes_service=None) ), label=("Objects API group"), - required=False, + required=True, help_text=_("Which Objects API group to use."), ) objecttype_uuid = serializers.UUIDField( label=_("objecttype"), - required=False, + required=True, help_text=_("UUID of the objecttype in the Objecttypes API. "), ) objecttype_version = serializers.IntegerField( label=_("objecttype version"), - required=False, + required=True, help_text=_("Version of the objecttype in the Objecttypes API."), ) auth_attribute_path = serializers.ListField( child=serializers.CharField(label=_("Segment of a JSON path")), label=_("Path to auth attribute (e.g. BSN/KVK) in objects"), help_text=_( - "This is used to perform validation to verify that the authenticated user is the owner of the object." + "This is used to perform validation to verify that the authenticated " + "user is the owner of the object." ), allow_empty=False, required=True, @@ -72,5 +76,5 @@ class ObjectsAPIOptionsSerializer(JsonSchemaSerializerMixin, serializers.Seriali variables_mapping = ObjecttypeVariableMappingSerializer( label=_("variables mapping"), many=True, - required=False, + required=True, ) diff --git a/src/openforms/prefill/contrib/objects_api/plugin.py b/src/openforms/prefill/contrib/objects_api/plugin.py index f0467796ad..6414f02172 100644 --- a/src/openforms/prefill/contrib/objects_api/plugin.py +++ b/src/openforms/prefill/contrib/objects_api/plugin.py @@ -1,6 +1,5 @@ import logging -from django.core.exceptions import PermissionDenied from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -10,7 +9,6 @@ from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig from openforms.contrib.objects_api.ownership_validation import validate_object_ownership -from openforms.logging import logevent from openforms.registrations.contrib.objects_api.models import ObjectsAPIConfig from openforms.submissions.models import Submission from openforms.typing import JSONEncodable, JSONObject @@ -32,33 +30,14 @@ class ObjectsAPIPrefill(BasePlugin[ObjectsAPIOptions]): options = ObjectsAPIOptionsSerializer def verify_initial_data_ownership( - self, submission: Submission, prefill_options: dict + self, submission: Submission, prefill_options: ObjectsAPIOptions ) -> None: assert submission.initial_data_reference - api_group = ObjectsAPIGroupConfig.objects.filter( - pk=prefill_options.get("objects_api_group") - ).first() - - if not api_group: - logger.info( - "No api group found to perform initial_data_reference ownership check for submission %s with options %s", - submission, - prefill_options, - ) - return - - auth_attribute_path = prefill_options.get("auth_attribute_path") - if not auth_attribute_path: - logger.info( - "Cannot perform initial data ownership check, because `auth_attribute_path` is missing from %s", - prefill_options, - ) - logevent.object_ownership_check_improperly_configured( - submission, plugin=self - ) - raise PermissionDenied( - f"`auth_attribute_path` missing from options {prefill_options}, cannot perform initial data ownership check" - ) + api_group = prefill_options["objects_api_group"] + assert api_group, "Can't do anything useful without an API group" + + auth_attribute_path = prefill_options["auth_attribute_path"] + assert auth_attribute_path, "Auth attribute path may not be empty" with get_objects_client(api_group) as client: validate_object_ownership(submission, client, auth_attribute_path, self) diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml index 614f90c9f6..b7e744516c 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_called_if_initial_data_reference_specified.yaml @@ -15,10 +15,10 @@ interactions: User-Agent: - python-requests/2.32.2 method: GET - uri: http://localhost:8002/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952 + uri: http://localhost:8002/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952","uuid":"bd6a9316-f721-40e4-9d4d-73ada5590952","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea","uuid":"c7ec7188-c8f5-4c13-bd74-942468077eea","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-03","endAt":null,"registrationAt":"2024-12-03","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, PUT, PATCH, DELETE, HEAD, OPTIONS @@ -33,11 +33,59 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 13:21:31 GMT + - Tue, 03 Dec 2024 16:13:43 GMT Referrer-Policy: - same-origin Server: - - nginx/1.27.0 + - nginx/1.27.3 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea","uuid":"c7ec7188-c8f5-4c13-bd74-942468077eea","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-03","endAt":null,"registrationAt":"2024-12-03","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 03 Dec 2024 16:13:43 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 Vary: - origin X-Content-Type-Options: diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail.yaml new file mode 100644 index 0000000000..9abedde96b --- /dev/null +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/ObjectsAPIPrefillDataOwnershipCheckTests.test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail.yaml @@ -0,0 +1,50 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea","uuid":"c7ec7188-c8f5-4c13-bd74-942468077eea","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-03","endAt":null,"registrationAt":"2024-12-03","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Tue, 03 Dec 2024 16:33:22 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +version: 1 diff --git a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml index 2189fc88bf..0a8e7a51bd 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml +++ b/src/openforms/prefill/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIPrefillDataOwnershipCheckTests/setUpTestData.yaml @@ -2,7 +2,7 @@ interactions: - request: body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, - "startAt": "2024-11-26"}}' + "startAt": "2024-12-03"}}' headers: Accept: - '*/*' @@ -24,7 +24,7 @@ interactions: uri: http://localhost:8002/api/v2/objects response: body: - string: '{"url":"http://objects-web:8000/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952","uuid":"bd6a9316-f721-40e4-9d4d-73ada5590952","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-11-26","endAt":null,"registrationAt":"2024-11-26","correctionFor":null,"correctedBy":null}}' + string: '{"url":"http://objects-web:8000/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea","uuid":"c7ec7188-c8f5-4c13-bd74-942468077eea","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-03","endAt":null,"registrationAt":"2024-12-03","correctionFor":null,"correctedBy":null}}' headers: Allow: - GET, POST, HEAD, OPTIONS @@ -39,13 +39,13 @@ interactions: Cross-Origin-Opener-Policy: - same-origin Date: - - Tue, 26 Nov 2024 13:21:30 GMT + - Tue, 03 Dec 2024 16:13:43 GMT Location: - - http://localhost:8002/api/v2/objects/bd6a9316-f721-40e4-9d4d-73ada5590952 + - http://localhost:8002/api/v2/objects/c7ec7188-c8f5-4c13-bd74-942468077eea Referrer-Policy: - same-origin Server: - - nginx/1.27.0 + - nginx/1.27.3 Vary: - origin X-Content-Type-Options: diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 0ca437c79e..371a47c7e1 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -1,5 +1,4 @@ from pathlib import Path -from unittest.mock import patch from django.core.exceptions import PermissionDenied from django.test import TestCase, tag @@ -7,17 +6,15 @@ from openforms.authentication.service import AuthAttribute from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.helpers import prepare_data_for_registration -from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory from openforms.forms.tests.factories import ( FormFactory, FormRegistrationBackendFactory, - FormStepFactory, FormVariableFactory, ) from openforms.logging.models import TimelineLogProxy from openforms.prefill.service import prefill_variables -from openforms.submissions.tests.factories import SubmissionStepFactory +from openforms.submissions.tests.factories import SubmissionFactory from openforms.utils.tests.vcr import OFVCRMixin, with_setup_test_data_vcr TEST_FILES = (Path(__file__).parent / "files").resolve() @@ -29,11 +26,23 @@ class ObjectsAPIPrefillDataOwnershipCheckTests(OFVCRMixin, TestCase): @classmethod def setUpTestData(cls): + """ + Set up a form with basic configuration: + + * 1 formstep, with a component 'postcode' + * 3 registration backends (email: irrelevant, objects API: different group, + objects API: relevant) + * 2 user defined form variables: + + * one to to the objects API prefill + * one to get the mapped variable assigned to + """ super().setUpTestData() cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( for_test_docker_compose=True ) + cls.objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() with with_setup_test_data_vcr(cls.VCR_TEST_FILES, cls.__qualname__): with get_objects_client(cls.objects_api_group_used) as client: @@ -46,12 +55,18 @@ def setUpTestData(cls): ) cls.object_ref = object["uuid"] - cls.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( - for_test_docker_compose=True + cls.form = FormFactory.create( + generate_minimal_setup=True, + formstep__form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + } + ], + }, ) - cls.objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() - - cls.form = FormFactory.create() # An objects API backend with a different API group FormRegistrationBackendFactory.create( form=cls.form, @@ -77,28 +92,19 @@ def setUpTestData(cls): }, ) - cls.form_step = FormStepFactory.create( - form_definition__configuration={ - "components": [ - { - "type": "postcode", - "key": "postcode", - "inputMask": "9999 AA", - } - ] - } - ) + FormVariableFactory.create(form=cls.form, key="voornamen", user_defined=True) cls.variable = FormVariableFactory.create( - key="voornamen", - form=cls.form_step.form, + form=cls.form, + key="prefillData", + user_defined=True, prefill_plugin="objects_api", prefill_attribute="", prefill_options={ "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", "objects_api_group": cls.objects_api_group_used.pk, + "objecttype_uuid": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", "objecttype_version": 1, - "auth_attribute_path": ["nested", "bsn"], + "auth_attribute_path": ["bsn"], "variables_mapping": [ {"variable_key": "voornamen", "target_path": ["some", "path"]}, ], @@ -108,137 +114,77 @@ def setUpTestData(cls): def test_verify_initial_data_ownership_called_if_initial_data_reference_specified( self, ): - submission_step = SubmissionStepFactory.create( - submission__form=self.form_step.form, - form_step=self.form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, + submission = SubmissionFactory.create( + form=self.form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, ) - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership" - ) as mock_validate_object_ownership: - prefill_variables(submission=submission_step.submission) - - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission_step.submission) - self.assertEqual( - call.args[1].base_url, - self.objects_api_group_used.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["nested", "bsn"]) - - logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) + prefill_variables(submission=submission) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 - ) + logs = TimelineLogProxy.objects.for_object(submission) + self.assertEqual(logs.filter_event("object_ownership_check_success").count(), 1) + self.assertEqual(logs.filter_event("prefill_retrieve_success").count(), 1) def test_verify_initial_data_ownership_raising_errors_causes_prefill_to_fail(self): - submission_step = SubmissionStepFactory.create( - submission__form=self.form_step.form, - form_step=self.form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, + # configure an invalid path, which causes errors during validation + self.variable.prefill_options["auth_attribute_path"] = ["nested", "bsn"] + self.variable.save() + submission = SubmissionFactory.create( + form=self.form, + # valid BSN, invalid config + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, ) - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: - with self.assertRaises(PermissionDenied): - prefill_variables(submission=submission_step.submission) - - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission_step.submission) - self.assertEqual( - call.args[1].base_url, - self.objects_api_group_used.objects_service.api_root, - ) - self.assertEqual(call.args[2], ["nested", "bsn"]) + with self.assertRaises(PermissionDenied): + prefill_variables(submission=submission) - logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 - ) + logs = TimelineLogProxy.objects.for_object(submission) + self.assertEqual(logs.filter_event("prefill_retrieve_failure").count(), 1) + self.assertFalse(logs.filter_event("object_ownership_check_success").exists()) + self.assertFalse(logs.filter_event("prefill_retrieve_success").exists()) def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failing_prefill( self, ): - del self.variable.prefill_options["auth_attribute_path"] - self.variable.save() - submission_step = SubmissionStepFactory.create( - submission__form=self.form_step.form, - form_step=self.form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, - ) - - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", - ) as mock_validate_object_ownership: - with self.assertRaises(PermissionDenied): - prefill_variables(submission=submission_step.submission) - - self.assertEqual(mock_validate_object_ownership.call_count, 0) - - logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 - ) - self.assertEqual( - logs.filter( - extra_data__log_event="object_ownership_check_improperly_configured" - ).count(), - 1, - ) + with self.subTest("missing auth_attribute_path config option"): + # doesn't pass serializer validation, so prefill fails + del self.variable.prefill_options["auth_attribute_path"] + self.variable.save() + submission = SubmissionFactory.create( + form=self.form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) - def test_verify_initial_data_ownership_does_not_raise_errors_without_api_group( - self, - ): - self.variable.prefill_options["objects_api_group"] = ( - ObjectsAPIGroupConfig.objects.last().pk + 1 - ) - self.variable.save() - submission_step = SubmissionStepFactory.create( - submission__form=self.form_step.form, - form_step=self.form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference=self.object_ref, - ) + prefill_variables(submission=submission) - with patch( - "openforms.prefill.contrib.objects_api.plugin.validate_object_ownership", - ) as mock_validate_object_ownership: - prefill_variables(submission=submission_step.submission) + logs = TimelineLogProxy.objects.for_object(submission) + self.assertEqual(logs.filter_event("prefill_retrieve_failure").count(), 1) + self.assertFalse( + logs.filter_event("object_ownership_check_success").exists() + ) + self.assertFalse(logs.filter_event("prefill_retrieve_success").exists()) + + with self.subTest("empty auth_attribute_path config option value"): + self.variable.prefill_options["auth_attribute_path"] = [] + self.variable.save() + submission = SubmissionFactory.create( + form=self.form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference=self.object_ref, + ) - self.assertEqual(mock_validate_object_ownership.call_count, 0) + prefill_variables(submission=submission) - logs = TimelineLogProxy.objects.filter(object_id=submission_step.submission.id) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) - # Prefilling fails, because the API group does not exist - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 - ) + logs = TimelineLogProxy.objects.for_object(submission) + self.assertEqual(logs.filter_event("prefill_retrieve_failure").count(), 1) + self.assertFalse( + logs.filter_event("object_ownership_check_success").exists() + ) + self.assertFalse(logs.filter_event("prefill_retrieve_success").exists()) diff --git a/src/openforms/prefill/contrib/objects_api/typing.py b/src/openforms/prefill/contrib/objects_api/typing.py index 2f380f7033..36fb5e7e0e 100644 --- a/src/openforms/prefill/contrib/objects_api/typing.py +++ b/src/openforms/prefill/contrib/objects_api/typing.py @@ -11,6 +11,7 @@ class VariableMapping(TypedDict): class ObjectsAPIOptions(TypedDict): objects_api_group: ObjectsAPIGroupConfig - object_type_uuid: UUID + objecttype_uuid: UUID objecttype_version: int + auth_attribute_path: list[str] variables_mapping: list[VariableMapping] diff --git a/src/openforms/prefill/sources.py b/src/openforms/prefill/sources.py index ec3b4d1763..fd947c7780 100644 --- a/src/openforms/prefill/sources.py +++ b/src/openforms/prefill/sources.py @@ -1,7 +1,7 @@ import logging from collections import defaultdict -from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.exceptions import PermissionDenied import elasticapm from rest_framework.exceptions import ValidationError @@ -14,6 +14,7 @@ ) from openforms.typing import JSONEncodable +from .constants import IdentifierRoles from .registry import Registry logger = logging.getLogger(__name__) @@ -37,7 +38,9 @@ def fetch_prefill_values_from_attribute( for variable in submission_variables: plugin_id = variable.form_variable.prefill_plugin - identifier_role = variable.form_variable.prefill_identifier_role + identifier_role = IdentifierRoles( + variable.form_variable.prefill_identifier_role + ) attribute_name = variable.form_variable.prefill_attribute grouped_fields[plugin_id][identifier_role].append( @@ -48,7 +51,7 @@ def fetch_prefill_values_from_attribute( @elasticapm.capture_span(span_type="app.prefill") def invoke_plugin( - item: tuple[str, str, list[dict[str, str]]] + item: tuple[str, IdentifierRoles, list[dict[str, str]]] ) -> tuple[list[dict[str, str]], dict[str, JSONEncodable]]: plugin_id, identifier_role, fields = item plugin = register[plugin_id] @@ -98,28 +101,45 @@ def fetch_prefill_values_from_options( values: dict[str, JSONEncodable] = {} for variable in variables: plugin = register[variable.form_variable.prefill_plugin] + raw_options = variable.form_variable.prefill_options + + # validate the options before processing them + options_serializer = plugin.options(data=raw_options) + try: + options_serializer.is_valid(raise_exception=True) + except ValidationError as exc: + logevent.prefill_retrieve_failure(submission, plugin, exc) + continue + + plugin_options = options_serializer.validated_data # If an `initial_data_reference` was passed, we must verify that the # authenticated user is the owner of the referenced object if submission.initial_data_reference: try: - plugin.verify_initial_data_ownership( - submission, variable.form_variable.prefill_options + plugin.verify_initial_data_ownership(submission, plugin_options) + except PermissionDenied as exc: + # XXX: these log records will typically not be created in the DB because + # the transaction is rolled back as part of DRFs exception handler + logger.warning( + "Submission %s attempted to prefill the initial_data_reference %s " + "using the %r plugin, but the ownership check failed.", + submission.uuid, + submission.initial_data_reference, + plugin, + exc_info=exc, + extra={ + "submission": submission.uuid, + "plugin_cls": type(plugin), + "initial_data_reference": submission.initial_data_reference, + }, ) - except (PermissionDenied, ImproperlyConfigured) as exc: logevent.prefill_retrieve_failure(submission, plugin, exc) raise exc - options_serializer = plugin.options(data=variable.form_variable.prefill_options) - - try: - options_serializer.is_valid(raise_exception=True) - except ValidationError as exc: - logevent.prefill_retrieve_failure(submission, plugin, exc) - continue try: new_values = plugin.get_prefill_values_from_options( - submission, options_serializer.validated_data + submission, plugin_options ) except Exception as exc: logger.exception(f"exception in prefill plugin '{plugin.identifier}'") From cc9a84dd0e3c22d77f994f8b5ca336b575f8a271 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 3 Dec 2024 19:08:35 +0100 Subject: [PATCH 33/39] :art: [#4398] Decouple variables endpoint validation behaviour from plugin details The important pattern is that certain validation checks run, not the implementation details of the objects api prefill plugin. So, we can properly isolate this by setting up a different plugin registry and doing the dependency injection through a helper, applying the same pattern as with registration plugins. --- .../forms/tests/variables/test_viewset.py | 25 +- src/openforms/prefill/service.py | 14 +- .../prefill/tests/test_prefill_hook.py | 5 +- .../prefill/tests/test_prefill_variables.py | 434 +++++++++++------- src/openforms/prefill/tests/utils.py | 25 + 5 files changed, 310 insertions(+), 193 deletions(-) create mode 100644 src/openforms/prefill/tests/utils.py diff --git a/src/openforms/forms/tests/variables/test_viewset.py b/src/openforms/forms/tests/variables/test_viewset.py index 6d06bb57d7..279b89e8c9 100644 --- a/src/openforms/forms/tests/variables/test_viewset.py +++ b/src/openforms/forms/tests/variables/test_viewset.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from factory.django import FileField -from rest_framework import status +from rest_framework import serializers, status from rest_framework.reverse import reverse from rest_framework.test import APITestCase from zgw_consumers.constants import APITypes, AuthTypes @@ -23,6 +23,8 @@ FormStepFactory, FormVariableFactory, ) +from openforms.prefill.contrib.demo.plugin import DemoPrefill +from openforms.prefill.tests.utils import get_test_register, patch_prefill_registry from openforms.variables.constants import ( DataMappingTypes, FormVariableDataTypes, @@ -903,6 +905,23 @@ def test_validators_accepts_only_numeric_keys(self): self.assertEqual(status.HTTP_200_OK, response.status_code) def test_bulk_create_and_update_with_prefill_constraints(self): + # Isolate the prefill registry/plugins for this test - we care about the pattern, + # not about particular plugin implementation details + new_register = get_test_register() + + class OptionsSerializer(serializers.Serializer): + foo = serializers.CharField(required=True, allow_blank=False) + + class OptionsPrefill(DemoPrefill): + options = OptionsSerializer + + new_register("demo-options")(OptionsPrefill) + + # set up registry patching for the test + cm = patch_prefill_registry(new_register) + cm.__enter__() + self.addCleanup(lambda: cm.__exit__(None, None, None)) + user = StaffUserFactory.create(user_permissions=["change_form"]) self.client.force_authenticate(user) @@ -1033,9 +1052,9 @@ def test_bulk_create_and_update_with_prefill_constraints(self): "service_fetch_configuration": None, "data_type": FormVariableDataTypes.string, "source": FormVariableSources.user_defined, - "prefill_plugin": "objects_api", + "prefill_plugin": "demo-options", "prefill_attribute": "", - "prefill_options": {"foo": "bar", "auth_attribute_path": ["bsn"]}, + "prefill_options": {"foo": "bar"}, } ] diff --git a/src/openforms/prefill/service.py b/src/openforms/prefill/service.py index e6e77249ba..8f37ffe67a 100644 --- a/src/openforms/prefill/service.py +++ b/src/openforms/prefill/service.py @@ -39,7 +39,10 @@ import elasticapm -from openforms.formio.service import FormioConfigurationWrapper +from openforms.formio.service import ( + FormioConfigurationWrapper, + normalize_value_for_component, +) from openforms.submissions.models import Submission from openforms.submissions.models.submission_value_variable import ( SubmissionValueVariable, @@ -47,7 +50,7 @@ from openforms.typing import JSONEncodable from openforms.variables.constants import FormVariableSources -from .registry import Registry +from .registry import Registry, register as default_register from .sources import ( fetch_prefill_values_from_attribute, fetch_prefill_values_from_options, @@ -71,9 +74,6 @@ def inject_prefill( The prefill values are looped over by key: value, and for each value the matching component is looked up to normalize it in the context of the component. """ - - from openforms.formio.service import normalize_value_for_component - prefilled_data = submission.get_prefilled_data() for key, prefill_value in prefilled_data.items(): try: @@ -111,10 +111,6 @@ def prefill_variables(submission: Submission, register: Registry | None = None) be used to fetch the value. If ``register`` is not specified, the default registry instance will be used. """ - from openforms.formio.service import normalize_value_for_component - - from .registry import register as default_register - register = register or default_register state = submission.load_submission_value_variables_state() diff --git a/src/openforms/prefill/tests/test_prefill_hook.py b/src/openforms/prefill/tests/test_prefill_hook.py index d0302bd2dd..3fb97e08f7 100644 --- a/src/openforms/prefill/tests/test_prefill_hook.py +++ b/src/openforms/prefill/tests/test_prefill_hook.py @@ -21,10 +21,9 @@ from ..contrib.demo.plugin import DemoPrefill from ..registry import Registry, register as prefill_register from ..service import inject_prefill, prefill_variables +from .utils import get_test_register -register = Registry() - -register("demo")(DemoPrefill) +register = get_test_register() CONFIGURATION = { "display": "form", diff --git a/src/openforms/prefill/tests/test_prefill_variables.py b/src/openforms/prefill/tests/test_prefill_variables.py index 4397178f8a..b053d609bd 100644 --- a/src/openforms/prefill/tests/test_prefill_variables.py +++ b/src/openforms/prefill/tests/test_prefill_variables.py @@ -4,6 +4,7 @@ from django.test import RequestFactory, TestCase, TransactionTestCase, tag import requests_mock +from rest_framework import serializers from zgw_consumers.test.factories import ServiceFactory from openforms.authentication.service import AuthAttribute @@ -20,12 +21,15 @@ ) from openforms.logging.models import TimelineLogProxy from openforms.submissions.constants import SubmissionValueVariableSources +from openforms.submissions.models import Submission from openforms.submissions.tests.factories import ( SubmissionFactory, SubmissionStepFactory, ) +from ..contrib.demo.plugin import DemoPrefill from ..service import prefill_variables +from .utils import get_test_register CONFIGURATION = { "display": "form", @@ -118,67 +122,6 @@ def test_applying_prefill_plugin_from_user_defined_with_attribute(self, m_prefil SubmissionValueVariableSources.prefill, submission_variable.source ) - @patch( - "openforms.prefill.service.fetch_prefill_values_from_options", - return_value={"voornamen": "Not so random string"}, - ) - def test_applying_prefill_plugin_from_user_defined_with_options(self, m_prefill): - submission = SubmissionFactory.create() - FormVariableFactory.create( - key="voornamen", - form=submission.form, - prefill_plugin="objects_api", - prefill_options={ - "objects_api_group": 1, - "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", - "objecttype_version": 3, - "variables_mapping": [ - {"variable_key": "voornamen", "target_path": ["some", "path"]}, - ], - }, - ) - - prefill_variables(submission=submission) - - submission_value_variables_state = ( - submission.load_submission_value_variables_state() - ) - - self.assertEqual(1, len(submission_value_variables_state.variables)) - - submission_variable = submission_value_variables_state.get_variable( - key="voornamen" - ) - - self.assertEqual("Not so random string", submission_variable.value) - self.assertEqual( - SubmissionValueVariableSources.prefill, submission_variable.source - ) - - def test_applying_prefill_plugin_from_user_defined_with_invalid_options(self): - submission = SubmissionFactory.create() - FormVariableFactory.create( - key="voornamen", - form=submission.form, - prefill_plugin="objects_api", - prefill_options={ - "objects_api_group": "Wrong value", - "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", - "objecttype_version": 3, - "variables_mapping": [ - {"variable_key": "voornamen", "target_path": ["some", "path"]}, - ], - }, - ) - - prefill_variables(submission=submission) - - self.assertEqual(TimelineLogProxy.objects.count(), 1) - logs = TimelineLogProxy.objects.get() - - self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_failure") - self.assertIn("objects_api_group", logs.extra_data["error"]) - @patch( "openforms.prefill.service.fetch_prefill_values_from_attribute", return_value={"postcode": "1015CJ", "birthDate": "19990615"}, @@ -280,6 +223,258 @@ def test_prefill_variables_are_retrieved_when_form_variables_deleted(self): self.assertEqual(2, len(prefill_variables)) +prefill_from_options_register = get_test_register() + + +class OptionsSerializer(serializers.Serializer): + var_key = serializers.CharField(required=True) + var_value = serializers.CharField(required=True) + crash_ownership_check = serializers.BooleanField(default=False, required=False) + + +@prefill_from_options_register("ownership-check-passes") +class OwnershipCheckPassesPlugin(DemoPrefill): + options = OptionsSerializer + + def verify_initial_data_ownership( + self, submission: Submission, prefill_options + ) -> None: + assert prefill_options + if prefill_options["crash_ownership_check"]: + raise Exception("crash and burn") + + @classmethod + def get_prefill_values_from_options(cls, submission: Submission, options): + return {options["var_key"]: options["var_value"]} + + +@prefill_from_options_register("ownership-check-fails") +class OwnershipCheckFailsPlugin(DemoPrefill): + options = OptionsSerializer + + def verify_initial_data_ownership( + self, submission: Submission, prefill_options + ) -> None: + raise PermissionDenied("you shall not pass") + + @classmethod + def get_prefill_values_from_options(cls, submission: Submission, options): + return {options["var_key"]: options["var_value"]} + + +class PrefillVariablesFromOptionsTests(TestCase): + @patch( + "openforms.prefill.service.fetch_prefill_values_from_options", + return_value={"voornamen": "Not so random string"}, + ) + def test_applying_prefill_plugin_from_user_defined_with_options(self, m_prefill): + submission = SubmissionFactory.create() + FormVariableFactory.create( + key="voornamen", + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": 1, + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "voornamen", "target_path": ["some", "path"]}, + ], + }, + ) + + prefill_variables(submission=submission) + + submission_value_variables_state = ( + submission.load_submission_value_variables_state() + ) + + self.assertEqual(1, len(submission_value_variables_state.variables)) + + submission_variable = submission_value_variables_state.get_variable( + key="voornamen" + ) + + self.assertEqual("Not so random string", submission_variable.value) + self.assertEqual( + SubmissionValueVariableSources.prefill, submission_variable.source + ) + + def test_applying_prefill_plugin_from_user_defined_with_invalid_options(self): + submission = SubmissionFactory.create() + FormVariableFactory.create( + key="voornamen", + form=submission.form, + prefill_plugin="objects_api", + prefill_options={ + "objects_api_group": "Wrong value", + "objecttype_uuid": "8e46e0a5-b1b4-449b-b9e9-fa3cea655f48", + "objecttype_version": 3, + "variables_mapping": [ + {"variable_key": "voornamen", "target_path": ["some", "path"]}, + ], + }, + ) + + prefill_variables(submission=submission) + + self.assertEqual(TimelineLogProxy.objects.count(), 1) + logs = TimelineLogProxy.objects.get() + + self.assertEqual(logs.extra_data["log_event"], "prefill_retrieve_failure") + self.assertIn("objects_api_group", logs.extra_data["error"]) + + @tag("gh-4398") + def test_verify_initial_data_ownership_only_called_with_initial_data_reference( + self, + ): + form = FormFactory.create( + generate_minimal_setup=True, + formstep__form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + } + ], + }, + ) + FormVariableFactory.create(form=form, key="voornamen", user_defined=True) + FormVariableFactory.create( + form=form, + key="prefillData", + user_defined=True, + prefill_plugin="ownership-check-fails", + prefill_options={ + "var_key": "voornamen", + "var_value": "foo, bar", + }, + ) + + with self.subTest("called with initial data reference"): + submission1 = SubmissionFactory.create( + form=form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference="some reference", + ) + + with self.assertRaises(PermissionDenied): + prefill_variables( + submission=submission1, register=prefill_from_options_register + ) + + logs = TimelineLogProxy.objects.for_object(submission1) + self.assertEqual(logs.filter_event("prefill_retrieve_failure").count(), 1) + self.assertFalse( + logs.filter_event("object_ownership_check_success").exists() + ) + self.assertFalse(logs.filter_event("prefill_retrieve_success").exists()) + + with self.subTest("called without initial data reference"): + submission2 = SubmissionFactory.create( + form=form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference="", + ) + try: + prefill_variables( + submission=submission2, register=prefill_from_options_register + ) + except PermissionDenied as exc: + raise self.failureException("Ownerhip check not expected") from exc + + logs = TimelineLogProxy.objects.for_object(submission2) + self.assertEqual(logs.filter_event("prefill_retrieve_success").count(), 1) + self.assertFalse(logs.filter_event("prefill_retrieve_failure").exists()) + self.assertFalse( + logs.filter_event("object_ownership_check_success").exists() + ) + + @tag("gh-4398") + def test_verify_initial_data_ownership_error_fails_prefill_entirely(self): + # These situations need to be visible in Sentry/error monitoring as they're + # unexpected crashes (and likely programming mistakes) + form = FormFactory.create( + generate_minimal_setup=True, + formstep__form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + } + ], + }, + ) + FormVariableFactory.create( + form=form, + key="prefillData", + user_defined=True, + prefill_plugin="ownership-check-passes", + prefill_options={ + "var_key": "voornamen", + "var_value": "foo, bar", + "crash_ownership_check": True, + }, + ) + + submission = SubmissionFactory.create( + form=form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference="some reference", + ) + + with self.assertRaisesMessage(Exception, "crash and burn"): + prefill_variables( + submission=submission, register=prefill_from_options_register + ) + + logs = TimelineLogProxy.objects.for_object(submission) + self.assertFalse(logs.filter_event("prefill_retrieve_failure").exists()) + self.assertFalse(logs.filter_event("prefill_retrieve_success").exists()) + + def test_successfull_verification_runs_prefill(self): + form = FormFactory.create( + generate_minimal_setup=True, + formstep__form_definition__configuration={ + "components": [ + { + "type": "postcode", + "key": "postcode", + "inputMask": "9999 AA", + } + ], + }, + ) + FormVariableFactory.create(form=form, key="voornamen", user_defined=True) + FormVariableFactory.create( + form=form, + key="prefillData", + user_defined=True, + prefill_plugin="ownership-check-passes", + prefill_options={ + "var_key": "voornamen", + "var_value": "foo, bar", + "crash_ownership_check": False, + }, + ) + submission = SubmissionFactory.create( + form=form, + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, + initial_data_reference="some reference", + ) + + prefill_variables(submission=submission, register=prefill_from_options_register) + + variables_state = submission.load_submission_value_variables_state() + self.assertEqual(variables_state.get_data()["voornamen"], "foo, bar") + + class PrefillVariablesTransactionTests(TransactionTestCase): @requests_mock.Mocker() @patch("openforms.contrib.haal_centraal.models.HaalCentraalConfig.get_solo") @@ -319,120 +514,3 @@ def test_no_success_message_on_failure(self, m, m_solo): for log in logs: self.assertNotEqual(log.event, "prefill_retrieve_success") - - @tag("gh-4398") - def test_verify_initial_data_ownership(self): - form_step = FormStepFactory.create( - form_definition__configuration={ - "components": [ - { - "type": "postcode", - "key": "postcode", - "inputMask": "9999 AA", - } - ] - } - ) - variable = FormVariableFactory.create( - key="voornamen", - form=form_step.form, - prefill_plugin="demo", - prefill_attribute="", - prefill_options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": 1, - "objecttype_version": 1, - }, - ) - - with self.subTest( - "verify_initial_data_ownership is not called if initial_data_reference is not specified" - ): - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - ) - - with patch( - "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" - ) as mock_verify_ownership: - with patch( - "openforms.prefill.contrib.demo.plugin.DemoPrefill.get_prefill_values_from_options", - return_value={"postcode": "1234AB"}, - ): - prefill_variables(submission=submission_step.submission) - - mock_verify_ownership.assert_not_called() - - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 - ) - - with self.subTest( - "verify_initial_data_ownership is called if initial_data_reference is specified" - ): - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference="1234", - ) - - with patch( - "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership" - ) as mock_verify_ownership: - with patch( - "openforms.prefill.contrib.demo.plugin.DemoPrefill.get_prefill_values_from_options", - return_value={"postcode": "1234AB"}, - ): - prefill_variables(submission=submission_step.submission) - - mock_verify_ownership.assert_called_once_with( - submission_step.submission, variable.prefill_options - ) - - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 1 - ) - - with self.subTest( - "verify_initial_data_ownership raising error causes prefill to fail" - ): - submission_step = SubmissionStepFactory.create( - submission__form=form_step.form, - form_step=form_step, - submission__auth_info__value="999990676", - submission__auth_info__attribute=AuthAttribute.bsn, - submission__initial_data_reference="1234", - ) - - with patch( - "openforms.prefill.contrib.demo.plugin.DemoPrefill.verify_initial_data_ownership", - side_effect=PermissionDenied, - ) as mock_verify_ownership: - with self.assertRaises(PermissionDenied): - prefill_variables(submission=submission_step.submission) - - mock_verify_ownership.assert_called_once_with( - submission_step.submission, variable.prefill_options - ) - - logs = TimelineLogProxy.objects.filter( - object_id=submission_step.submission.id - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_success").count(), 0 - ) - self.assertEqual( - logs.filter(extra_data__log_event="prefill_retrieve_failure").count(), 1 - ) diff --git a/src/openforms/prefill/tests/utils.py b/src/openforms/prefill/tests/utils.py new file mode 100644 index 0000000000..a1695f450e --- /dev/null +++ b/src/openforms/prefill/tests/utils.py @@ -0,0 +1,25 @@ +from contextlib import contextmanager +from unittest.mock import patch + +from ..contrib.demo.plugin import DemoPrefill +from ..registry import Registry + + +def get_test_register() -> Registry: + register = Registry() + register("demo")(DemoPrefill) + return register + + +@contextmanager +def patch_prefill_registry(new_register: Registry | None = None): + if new_register is None: + new_register = get_test_register() + + with ( + patch("openforms.prefill.service.default_register", new=new_register), + patch( + "openforms.forms.api.serializers.form_variable.register", new=new_register + ), + ): + yield From e6fe66f80bd3f5ecd993da74a44ab0a2cec50a24 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 15:52:09 +0100 Subject: [PATCH 34/39] :art: [#4398] Clean up test code Dropped the bits that are not relevant. --- .../tests/test_ownership_validation.py | 240 +++++------------- .../test_initial_data_ownership_validation.py | 31 +-- 2 files changed, 60 insertions(+), 211 deletions(-) diff --git a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py index 4ed0f4dd51..ba9ac0c643 100644 --- a/src/openforms/contrib/objects_api/tests/test_ownership_validation.py +++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py @@ -10,7 +10,6 @@ from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.helpers import prepare_data_for_registration from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory -from openforms.forms.tests.factories import FormRegistrationBackendFactory from openforms.logging.models import TimelineLogProxy from openforms.registrations.contrib.objects_api.plugin import ObjectsAPIRegistration from openforms.submissions.tests.factories import SubmissionFactory @@ -59,51 +58,30 @@ def test_user_is_owner_of_object(self): initial_data_reference=self.object_ref, ) - # An objects API backend with a different API group - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": 5, - "objecttype_version": 1, - }, - ) - # Another backend that should be ignored - FormRegistrationBackendFactory.create(form=submission.form, backend="email") - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) + try: + validate_object_ownership(submission, client, ["bsn"], PLUGIN) + except PermissionDenied as exc: + raise self.failureException( + "BSN in submission is owner of data" + ) from exc @tag("gh-4398") def test_permission_denied_if_user_is_not_logged_in(self): submission = SubmissionFactory.create(initial_data_reference=self.object_ref) + assert not submission.is_authenticated + + with ( + get_objects_client(self.objects_api_group_used) as client, + self.assertRaisesMessage( + PermissionDenied, "Cannot pass data reference as anonymous user" + ), + ): + validate_object_ownership(submission, client, ["bsn"], PLUGIN) - with get_objects_client(self.objects_api_group_used) as client: - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) - self.assertEqual( - str(cm.exception), "Cannot pass data reference as anonymous user" - ) - - logs = TimelineLogProxy.objects.filter(object_id=submission.id) + logs = TimelineLogProxy.objects.for_object(submission) self.assertEqual( - logs.filter( - extra_data__log_event="object_ownership_check_anonymous_user" - ).count(), - 1, + logs.filter_event("object_ownership_check_anonymous_user").count(), 1 ) @tag("gh-4398") @@ -114,30 +92,16 @@ def test_user_is_not_owner_of_object(self): initial_data_reference=self.object_ref, ) - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - - with get_objects_client(self.objects_api_group_used) as client: - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) - self.assertEqual( - str(cm.exception), "User is not the owner of the referenced object" - ) + with ( + get_objects_client(self.objects_api_group_used) as client, + self.assertRaisesMessage( + PermissionDenied, "User is not the owner of the referenced object" + ), + ): + validate_object_ownership(submission, client, ["bsn"], PLUGIN) - logs = TimelineLogProxy.objects.filter(object_id=submission.id) - self.assertEqual( - logs.filter(extra_data__log_event="object_ownership_check_failure").count(), - 1, - ) + logs = TimelineLogProxy.objects.for_object(submission) + self.assertEqual(logs.filter_event("object_ownership_check_failure").count(), 1) @tag("gh-4398") def test_user_is_not_owner_of_object_nested_auth_attribute(self): @@ -157,24 +121,13 @@ def test_user_is_not_owner_of_object_nested_auth_attribute(self): initial_data_reference=object_ref, ) - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - - with get_objects_client(self.objects_api_group_used) as client: - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, ["nested", "bsn"], PLUGIN) - self.assertEqual( - str(cm.exception), "User is not the owner of the referenced object" - ) + with ( + get_objects_client(self.objects_api_group_used) as client, + self.assertRaisesMessage( + PermissionDenied, "User is not the owner of the referenced object" + ), + ): + validate_object_ownership(submission, client, ["nested", "bsn"], PLUGIN) @tag("gh-4398") def test_ownership_check_fails_if_auth_attribute_path_is_badly_configured(self): @@ -194,37 +147,26 @@ def test_ownership_check_fails_if_auth_attribute_path_is_badly_configured(self): initial_data_reference=object_ref, ) - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - - with self.subTest("empty path"): - with get_objects_client(self.objects_api_group_used) as client: - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership(submission, client, [], PLUGIN) - self.assertEqual( - str(cm.exception), - "Could not verify if user is owner of the referenced object", - ) - - with self.subTest("non existent path"): - with get_objects_client(self.objects_api_group_used) as client: - with self.assertRaises(PermissionDenied) as cm: - validate_object_ownership( - submission, client, ["this", "does", "not", "exist"], PLUGIN - ) - self.assertEqual( - str(cm.exception), - "Could not verify if user is owner of the referenced object", - ) + with get_objects_client(self.objects_api_group_used) as client: + with ( + self.subTest("empty path"), + self.assertRaisesMessage( + PermissionDenied, + "Could not verify if user is owner of the referenced object", + ), + ): + validate_object_ownership(submission, client, [], PLUGIN) + + with ( + self.subTest("non existent path"), + self.assertRaisesMessage( + PermissionDenied, + "Could not verify if user is owner of the referenced object", + ), + ): + validate_object_ownership( + submission, client, ["this", "does", "not", "exist"], PLUGIN + ) @tag("gh-4398") @patch( @@ -239,23 +181,10 @@ def test_request_exception_when_doing_permission_check(self, mock_get_object): submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, - initial_data_reference=self.object_ref, - ) - - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, + initial_data_reference="irrelevant", ) - - with self.assertRaises(PermissionDenied): - with get_objects_client(self.objects_api_group_used) as client: + with get_objects_client(self.objects_api_group_used) as client: + with self.assertRaises(PermissionDenied): validate_object_ownership(submission, client, ["bsn"], PLUGIN) @tag("gh-4398") @@ -271,60 +200,9 @@ def test_object_not_found_when_doing_permission_check(self, mock_get_object): submission = SubmissionFactory.create( auth_info__value="111222333", auth_info__attribute=AuthAttribute.bsn, - initial_data_reference=self.object_ref, - ) - - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=submission.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - - with self.assertRaises(PermissionDenied): - with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) - - @tag("gh-4398") - def test_no_backends_configured_does_not_raise_error( - self, - ): - """ - If the object could not be fetched due to misconfiguration, the ownership check - should not fail - """ - submission = SubmissionFactory.create( - auth_info__value="111222333", - auth_info__attribute=AuthAttribute.bsn, - initial_data_reference=self.object_ref, + initial_data_reference="irrelevant", ) - FormRegistrationBackendFactory.create(form=submission.form, backend="email") with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) - - @tag("gh-4398") - def test_backend_without_options_does_not_raise_error( - self, - ): - """ - If the object could not be fetched due to missing API group configuration, - the ownership check should not fail - """ - submission = SubmissionFactory.create( - auth_info__value="111222333", - auth_info__attribute=AuthAttribute.bsn, - initial_data_reference=self.object_ref, - ) - ObjectsAPIGroupConfigFactory.create(for_test_docker_compose=True) - FormRegistrationBackendFactory.create( - form=submission.form, backend="objects_api", options={} - ) - - with get_objects_client(self.objects_api_group_used) as client: - validate_object_ownership(submission, client, ["bsn"], PLUGIN) + with self.assertRaises(PermissionDenied): + validate_object_ownership(submission, client, ["bsn"], PLUGIN) diff --git a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 371a47c7e1..c3dbc7b227 100644 --- a/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/prefill/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -7,11 +7,7 @@ from openforms.contrib.objects_api.clients import get_objects_client from openforms.contrib.objects_api.helpers import prepare_data_for_registration from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory -from openforms.forms.tests.factories import ( - FormFactory, - FormRegistrationBackendFactory, - FormVariableFactory, -) +from openforms.forms.tests.factories import FormFactory, FormVariableFactory from openforms.logging.models import TimelineLogProxy from openforms.prefill.service import prefill_variables from openforms.submissions.tests.factories import SubmissionFactory @@ -67,31 +63,6 @@ def setUpTestData(cls): ], }, ) - # An objects API backend with a different API group - FormRegistrationBackendFactory.create( - form=cls.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": cls.objects_api_group_unused.pk, - "objecttype_version": 1, - }, - ) - # Another backend that should be ignored - FormRegistrationBackendFactory.create(form=cls.form, backend="email") - # The backend that should be used to perform the check - FormRegistrationBackendFactory.create( - form=cls.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": cls.objects_api_group_used.pk, - "objecttype_version": 1, - }, - ) - FormVariableFactory.create(form=cls.form, key="voornamen", user_defined=True) cls.variable = FormVariableFactory.create( form=cls.form, From cbc7bc9613d603b22fc782f338d3b42697c1cbd9 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 17:18:56 +0100 Subject: [PATCH 35/39] :recycle: [#4398] Update ownership check interface in registration plugin The task/generic machinery takes care of deserializing the plugin options and passes them to the hook for ownership verification, greatly simplifying the code that needs to be implemented. --- pyright.pyproject.toml | 1 + src/openforms/logging/logevent.py | 8 ---- ..._ownership_check_improperly_configured.txt | 4 -- src/openforms/registrations/base.py | 30 ++++++++----- .../contrib/objects_api/plugin.py | 44 ++++++------------- .../contrib/objects_api/typing.py | 11 ++--- src/openforms/registrations/tasks.py | 26 +++++++---- .../submissions/models/submission.py | 6 ++- 8 files changed, 62 insertions(+), 68 deletions(-) delete mode 100644 src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt diff --git a/pyright.pyproject.toml b/pyright.pyproject.toml index 7fa2502384..55892ac49c 100644 --- a/pyright.pyproject.toml +++ b/pyright.pyproject.toml @@ -30,6 +30,7 @@ include = [ "src/openforms/contrib/zgw/service.py", "src/openforms/contrib/objects_api/", # Registrations + "src/openforms/registrations/tasks.py", "src/openforms/registrations/contrib/email/config.py", "src/openforms/registrations/contrib/email/plugin.py", "src/openforms/registrations/contrib/stuf_zds/options.py", diff --git a/src/openforms/logging/logevent.py b/src/openforms/logging/logevent.py index bdb3e3251c..4c5fe0d91c 100644 --- a/src/openforms/logging/logevent.py +++ b/src/openforms/logging/logevent.py @@ -271,14 +271,6 @@ def object_ownership_check_anonymous_user(submission: Submission, plugin=None): ) -def object_ownership_check_improperly_configured(submission: Submission, plugin=None): - _create_log( - submission, - "object_ownership_check_improperly_configured", - plugin=plugin, - ) - - # - - - diff --git a/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt b/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt deleted file mode 100644 index 204868fde4..0000000000 --- a/src/openforms/logging/templates/logging/events/object_ownership_check_improperly_configured.txt +++ /dev/null @@ -1,4 +0,0 @@ -{% load i18n %} -{% blocktrans trimmed with plugin=log.fmt_plugin lead=log.fmt_lead %} - {{ lead }}: Registration plugin {{ plugin }} reported: cannot perform initial data reference ownership check due to missing `Path to auth attribute (e.g. BSN/KVK) in objects` in configuration. -{% endblocktrans %} diff --git a/src/openforms/registrations/base.py b/src/openforms/registrations/base.py index 14804549bf..efd30f0051 100644 --- a/src/openforms/registrations/base.py +++ b/src/openforms/registrations/base.py @@ -63,6 +63,25 @@ def update_payment_status( ) -> dict | None: raise NotImplementedError() + def verify_initial_data_ownership( + self, submission: Submission, options: OptionsT + ) -> None: + """ + Check that the submission user is the owner of the registration target. + + Registration backends can possibly update existing objects, which are + referenced through :attr:`submission.initial_data_reference`. These plugins + must check that the submission user is actually the 'owner' of this object. For + example, a permit request may have a BSN stored, or a case can have an + initiator/authorizee identified by a BSN/Chamber of Commerce number. + + :param submission: an active :class:`Submission` instance. + :param options: the deserialized plugin configuration options. + """ + raise NotImplementedError( + "You must implement the 'verify_initial_data_ownership' method." + ) + def pre_register_submission( self, submission: Submission, options: OptionsT ) -> PreRegistrationResult: @@ -84,14 +103,3 @@ def get_variables(self) -> list[FormVariable]: Return the static variables for this registration plugin. """ return [] - - def verify_initial_data_ownership(self, submission: Submission) -> None: - """ - Hook to check if the authenticated user is the owner of the object - referenced to by `initial_data_reference` - - :param submission: an active :class:`Submission` instance - """ - raise NotImplementedError( - "You must implement the 'verify_initial_data_ownership' method." - ) diff --git a/src/openforms/registrations/contrib/objects_api/plugin.py b/src/openforms/registrations/contrib/objects_api/plugin.py index 51bb7926d5..674b0c4d03 100644 --- a/src/openforms/registrations/contrib/objects_api/plugin.py +++ b/src/openforms/registrations/contrib/objects_api/plugin.py @@ -4,7 +4,6 @@ from functools import partial from typing import TYPE_CHECKING, Any, override -from django.core.exceptions import PermissionDenied from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -14,9 +13,7 @@ get_objects_client, get_objecttypes_client, ) -from openforms.contrib.objects_api.models import ObjectsAPIGroupConfig from openforms.contrib.objects_api.ownership_validation import validate_object_ownership -from openforms.logging import logevent from openforms.registrations.utils import execute_unless_result_exists from openforms.variables.service import get_static_variables @@ -61,6 +58,20 @@ def set_defaults(options: RegistrationOptions) -> None: "productaanvraag_type", global_config.productaanvraag_type ) + @override + def verify_initial_data_ownership( + self, submission: Submission, options: RegistrationOptions + ) -> None: + assert submission.initial_data_reference + api_group = options["objects_api_group"] + assert api_group, "Can't do anything useful without an API group" + + auth_attribute_path = options["auth_attribute_path"] + assert auth_attribute_path, "Auth attribute path may not be empty" + + with get_objects_client(api_group) as client: + validate_object_ownership(submission, client, auth_attribute_path, self) + @override def register_submission( self, submission: Submission, options: RegistrationOptions @@ -174,30 +185,3 @@ def update_payment_status( @override def get_variables(self) -> list[FormVariable]: return get_static_variables(variables_registry=variables_registry) - - def verify_initial_data_ownership(self, submission: Submission) -> None: - assert submission.registration_backend - assert submission.initial_data_reference - backend = submission.registration_backend - - api_group = ObjectsAPIGroupConfig.objects.filter( - pk=backend.options.get("objects_api_group") - ).first() - if not api_group: - return - - auth_attribute_path = backend.options.get("auth_attribute_path") - if not auth_attribute_path: - logger.error( - "Cannot perform initial data ownership check, because backend %s has no `auth_attribute_path` configured", - backend, - ) - logevent.object_ownership_check_improperly_configured( - submission, plugin=self - ) - raise PermissionDenied( - f"{backend} has no `auth_attribute_path` configured, cannot perform initial data ownership check" - ) - - with get_objects_client(api_group) as client: - validate_object_ownership(submission, client, auth_attribute_path, self) diff --git a/src/openforms/registrations/contrib/objects_api/typing.py b/src/openforms/registrations/contrib/objects_api/typing.py index c18a545f0d..00c0e624ca 100644 --- a/src/openforms/registrations/contrib/objects_api/typing.py +++ b/src/openforms/registrations/contrib/objects_api/typing.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, NotRequired, Required, TypeAlias, TypedDict +from typing import TYPE_CHECKING, Literal, NotRequired, Required, TypedDict from uuid import UUID -ConfigVersion: TypeAlias = Literal[1, 2] - if TYPE_CHECKING: from .models import ObjectsAPIGroupConfig +type ConfigVersion = Literal[1, 2] + + class CatalogueOption(TypedDict): domain: str rsin: str @@ -26,7 +27,7 @@ class _BaseRegistrationOptions(TypedDict, total=False): objecttype: Required[UUID] objecttype_version: Required[int] update_existing_object: Required[bool] - auth_attribute_path: list[str] + auth_attribute_path: Required[list[str]] # metadata of documents created in the documents API upload_submission_csv: bool @@ -71,5 +72,5 @@ class RegistrationOptionsV2(_BaseRegistrationOptions, total=False): geometry_variable_key: str -RegistrationOptions: TypeAlias = RegistrationOptionsV1 | RegistrationOptionsV2 +type RegistrationOptions = RegistrationOptionsV1 | RegistrationOptionsV2 """The Objects API registration options (either V1 or V2).""" diff --git a/src/openforms/registrations/tasks.py b/src/openforms/registrations/tasks.py index 826beeb388..0bda42a57f 100644 --- a/src/openforms/registrations/tasks.py +++ b/src/openforms/registrations/tasks.py @@ -75,11 +75,6 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: registration_plugin = get_registration_plugin(submission) - # If an `initial_data_reference` was passed, we must verify that the - # authenticated user is the owner of the referenced object - if registration_plugin and submission.initial_data_reference: - registration_plugin.verify_initial_data_ownership(submission) - with transaction.atomic(): if not registration_plugin: set_submission_reference(submission) @@ -87,6 +82,7 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: submission.save() return + assert submission.registration_backend is not None options_serializer = registration_plugin.configuration_options( data=submission.registration_backend.options, context={"validate_business_logic": False}, @@ -111,10 +107,20 @@ def pre_registration(submission_id: int, event: PostSubmissionEvents) -> None: ) submission.save() + plugin_options = options_serializer.validated_data with track_error(submission, event) as should_abort: - result = registration_plugin.pre_register_submission( - submission, options_serializer.validated_data - ) + # If an `initial_data_reference` was passed, we must verify that the + # authenticated user is the owner of the referenced object + if submission.initial_data_reference: + # may raise PermissionDenied + # XXX: audit logging inside this check is likely lost when the outer + # transaction block rolls back. See + # https://github.com/open-formulieren/open-forms/pull/4696/files#r1863209778 + registration_plugin.verify_initial_data_ownership( + submission, plugin_options + ) + + result = registration_plugin.pre_register_submission(submission, plugin_options) if should_abort: return @@ -236,7 +242,9 @@ def register_submission(submission_id: int, event: PostSubmissionEvents | str) - logevent.registration_skip(submission) return - registry = backend_config._meta.get_field("backend").registry + registry = backend_config._meta.get_field( + "backend" + ).registry # pyright: ignore[reportAttributeAccessIssue] backend = backend_config.backend logger.debug("Looking up plugin with unique identifier '%s'", backend) diff --git a/src/openforms/submissions/models/submission.py b/src/openforms/submissions/models/submission.py index 8bb9f7a642..68c3db52d1 100644 --- a/src/openforms/submissions/models/submission.py +++ b/src/openforms/submissions/models/submission.py @@ -375,7 +375,10 @@ def refresh_from_db(self, *args, **kwargs): del self._variables_state def save_registration_status( - self, status: RegistrationStatuses, result: dict, record_attempt: bool = False + self, + status: RegistrationStatuses, + result: dict | None, + record_attempt: bool = False, ) -> None: # combine the new result with existing data, where the new result overwrites # on key collisions. This allows storing intermediate results in the plugin @@ -383,6 +386,7 @@ def save_registration_status( if not self.registration_result and result is None: full_result = None else: + assert result is not None full_result = { **(self.registration_result or {}), **result, From 794ca2ee8312106eee4e8c0b2ff51d0fc9525bf0 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 17:44:48 +0100 Subject: [PATCH 36/39] :art: [#4398] Use dependency injection in unit tests Properly mock the registry of plugins instead of having to rely on particular demo plugins having certain behaviours or needing to mock implementation details in tests. --- .../tests/test_pre_registration.py | 105 ++++++++++++------ 1 file changed, 70 insertions(+), 35 deletions(-) diff --git a/src/openforms/registrations/tests/test_pre_registration.py b/src/openforms/registrations/tests/test_pre_registration.py index 66ba6c6b31..ec6f43cf76 100644 --- a/src/openforms/registrations/tests/test_pre_registration.py +++ b/src/openforms/registrations/tests/test_pre_registration.py @@ -7,18 +7,35 @@ from testfixtures import LogCapture from openforms.config.models import GlobalConfiguration +from openforms.forms.models import FormRegistrationBackend from openforms.registrations.base import PreRegistrationResult from openforms.registrations.contrib.zgw_apis.tests.factories import ( ZGWApiGroupConfigFactory, ) -from openforms.registrations.tasks import register_submission from openforms.submissions.constants import PostSubmissionEvents from openforms.submissions.tasks.registration import pre_registration from openforms.submissions.tests.factories import SubmissionFactory from openforms.utils.tests.logging import ensure_logger_level +from ..contrib.demo.plugin import DemoRegistration +from ..fields import RegistrationBackendChoiceField +from ..registry import Registry +from ..tasks import register_submission +from .utils import patch_registry as _patch_registry + + +# TODO: make this implementation the default, project-wide +def patch_registry(register: Registry): + model_field = FormRegistrationBackend._meta.get_field("backend") + assert isinstance(model_field, RegistrationBackendChoiceField) + return _patch_registry(model_field, register) + class PreRegistrationTests(TestCase): + def setUp(self): + super().setUp() + self.addCleanup(GlobalConfiguration.clear_cache) + def test_pre_registration_with_submission_not_completed(self): submission = SubmissionFactory.create() @@ -378,51 +395,69 @@ def test_traceback_removed_from_result_after_success(self, m_get_solo): @tag("gh-4398") def test_verify_initial_data_ownership(self): - with self.subTest( - "verify_initial_data_ownership is not called if no initial_data_reference is specified" - ): - submission = SubmissionFactory.create( - form__registration_backend="demo", + # set up a separate registry with plugins we control, to test the generic + # mechanism + register = Registry() + + class TestPlugin(DemoRegistration): + def verify_initial_data_ownership(self, submission, options): + if submission.initial_data_reference == "trigger-crash": + raise Exception("arbitrary crash!") + raise PermissionDenied("you shall not pass") + + register("ownership-check-fails")(TestPlugin) + + patcher = patch_registry(register) + patcher.__enter__() + self.addCleanup(lambda: patcher.__exit__(None, None, None)) + + with self.subTest("no ownership check without initial data reference"): + submission1 = SubmissionFactory.create( + form__registration_backend="ownership-check-fails", completed_not_preregistered=True, + initial_data_reference="", ) + assert not submission1.pre_registration_completed - with patch( - "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership" - ) as mock_verify_ownership: - pre_registration(submission.id, PostSubmissionEvents.on_completion) + pre_registration(submission1.id, PostSubmissionEvents.on_completion) - mock_verify_ownership.assert_not_called() + submission1.refresh_from_db() + self.assertTrue(submission1.pre_registration_completed) - with self.subTest( - "verify_initial_data_ownership is called if initial_data_reference exists is specified" - ): - submission = SubmissionFactory.create( - form__registration_backend="demo", + with self.subTest("ownership check runs when initial data reference given"): + submission2 = SubmissionFactory.create( + form__registration_backend="ownership-check-fails", completed_not_preregistered=True, - initial_data_reference="1234", + initial_data_reference="some reference", ) + assert not submission2.pre_registration_completed - with patch( - "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership" - ) as mock_verify_ownership: - pre_registration(submission.id, PostSubmissionEvents.on_completion) + pre_registration(submission2.id, PostSubmissionEvents.on_completion) - mock_verify_ownership.assert_called_once_with(submission) + submission2.refresh_from_db() + # False because the ownership check prevented it from completing + self.assertFalse(submission2.pre_registration_completed) + self.assertIn( + "PermissionDenied", submission2.registration_result["traceback"] + ) + self.assertIn( + "you shall not pass", submission2.registration_result["traceback"] + ) - with self.subTest( - "verify_initial_data_ownership raising error causes pre registration to fail" - ): - submission = SubmissionFactory.create( - form__registration_backend="demo", + with self.subTest("arbitrary errors in ownership check abort registration"): + submission3 = SubmissionFactory.create( + form__registration_backend="ownership-check-fails", completed_not_preregistered=True, - initial_data_reference="1234", + initial_data_reference="trigger-crash", ) + assert not submission3.pre_registration_completed - with patch( - "openforms.registrations.contrib.demo.plugin.DemoRegistration.verify_initial_data_ownership", - side_effect=PermissionDenied, - ) as mock_verify_ownership: - with self.assertRaises(PermissionDenied): - pre_registration(submission.id, PostSubmissionEvents.on_completion) + pre_registration(submission3.id, PostSubmissionEvents.on_completion) - mock_verify_ownership.assert_called_once_with(submission) + submission3.refresh_from_db() + # False because the ownership check prevented it from completing + self.assertFalse(submission3.pre_registration_completed) + self.assertIn("Exception", submission3.registration_result["traceback"]) + self.assertIn( + "arbitrary crash!", submission3.registration_result["traceback"] + ) From 20cb7d0c917d098e32e73c46d11cb8399fd470e3 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 17:50:43 +0100 Subject: [PATCH 37/39] :label: [#4398] Fix type errors in Objects API tests --- .../contrib/objects_api/tests/test_backend.py | 7 +++++++ .../objects_api/tests/test_backend_v1.py | 18 ++++++++++++++++++ .../objects_api/tests/test_backend_v2.py | 18 ++++++++++++++++++ .../objects_api/tests/test_serializer.py | 4 +++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_backend.py b/src/openforms/registrations/contrib/objects_api/tests/test_backend.py index 9c68a58e75..3000483a33 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_backend.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_backend.py @@ -298,6 +298,7 @@ def test_submission_with_objects_api_backend_create_and_update_object(self): "objecttype_version": 3, "objects_api_group": objects_api_group, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -328,6 +329,7 @@ def test_submission_with_objects_api_backend_create_and_update_object(self): "objecttype_version": 3, "objects_api_group": objects_api_group, "update_existing_object": True, + "auth_attribute_path": ["bsn"], }, ) @@ -364,6 +366,7 @@ def test_submission_with_objects_api_backend_create_and_update_object(self): "objecttype_version": 3, "objects_api_group": objects_api_group, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -393,6 +396,7 @@ def test_submission_with_objects_api_backend_create_and_update_object(self): "objecttype_version": 3, "objects_api_group": objects_api_group, "update_existing_object": True, + "auth_attribute_path": ["bsn"], }, ) @@ -447,6 +451,7 @@ def test_prefer_dynamic_resolution_over_fixed_url(self): "objecttype": UUID("527b8408-7421-4808-a744-43ccb7bdaaa2"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "attachment", @@ -517,6 +522,7 @@ def test_create_document_documenttype_dynamically_resolved(self): "objecttype": UUID("527b8408-7421-4808-a744-43ccb7bdaaa2"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "attachment", @@ -580,6 +586,7 @@ def test_allow_registration_with_unpublished_document_types(self): "objecttype": UUID("527b8408-7421-4808-a744-43ccb7bdaaa2"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "attachment", diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_backend_v1.py b/src/openforms/registrations/contrib/objects_api/tests/test_backend_v1.py index 7a64947a81..03f050cadf 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_backend_v1.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_backend_v1.py @@ -147,6 +147,7 @@ def test_submission_with_objects_api_backend_override_defaults(self): "objecttype_version": 1, "productaanvraag_type": "testproduct", "update_existing_object": False, + "auth_attribute_path": [], # `omschrijving` "PDF Informatieobjecttype other catalog": "informatieobjecttype_submission_report": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/f2908f6f-aa07-42ef-8760-74c5234f2d25", "upload_submission_csv": True, @@ -240,6 +241,7 @@ def test_submission_with_objects_api_backend_override_defaults_upload_csv_defaul "informatieobjecttype_submission_report": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/f2908f6f-aa07-42ef-8760-74c5234f2d25", "upload_submission_csv": True, "update_existing_object": False, + "auth_attribute_path": [], "organisatie_rsin": "123456782", "zaak_vertrouwelijkheidaanduiding": "geheim", "doc_vertrouwelijkheidaanduiding": "geheim", @@ -297,6 +299,7 @@ def test_submission_with_objects_api_backend_override_defaults_do_not_upload_csv "objects_api_group": self.objects_api_group, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -329,6 +332,7 @@ def test_submission_with_objects_api_backend_missing_csv_iotype(self): "objects_api_group": self.objects_api_group, "upload_submission_csv": True, "update_existing_object": False, + "auth_attribute_path": [], "informatieobjecttype_submission_csv": "", }, ) @@ -374,6 +378,7 @@ def test_submission_with_objects_api_backend_override_content_json(self): "objects_api_group": self.objects_api_group, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "content_json": textwrap.dedent( """ { @@ -438,6 +443,7 @@ def test_submission_with_objects_api_backend_use_config_defaults(self): "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -513,6 +519,7 @@ def test_submission_with_objects_api_backend_attachments(self): "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -623,6 +630,7 @@ def test_submission_with_objects_api_backend_attachments_specific_iotypen(self): "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -719,6 +727,7 @@ def test_submission_with_objects_api_backend_attachments_component_overwrites(se "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -802,6 +811,7 @@ def test_submission_with_objects_api_backend_attachments_component_inside_fields "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -861,6 +871,7 @@ def test_submission_with_objects_api_escapes_html(self): "content_json": content_template, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -906,6 +917,7 @@ def test_submission_with_payment(self): "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], }, ) @@ -943,6 +955,7 @@ def test_submission_with_auth_context_data(self): "informatieobjecttype_submission_report": "", "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "content_json": r"""{"auth": {% as_json variables.auth_context %}}""", }, ) @@ -1003,6 +1016,7 @@ def test_submission_with_auth_context_data_not_authenticated(self): "informatieobjecttype_submission_report": "", "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "content_json": r"""{"auth": {% as_json variables.auth_context %}}""", }, ) @@ -1057,6 +1071,7 @@ def test_cosign_info_available(self): "objecttype_version": 1, "productaanvraag_type": "-dummy-", "update_existing_object": False, + "auth_attribute_path": [], "content_json": textwrap.dedent( """ { @@ -1103,6 +1118,7 @@ def test_cosign_info_not_available(self): "objecttype_version": 1, "productaanvraag_type": "-dummy-", "update_existing_object": False, + "auth_attribute_path": [], "content_json": textwrap.dedent( """ { @@ -1152,6 +1168,7 @@ def test_cosign_info_no_cosign_date(self): "objecttype_version": 1, "productaanvraag_type": "-dummy-", "update_existing_object": False, + "auth_attribute_path": [], "content_json": textwrap.dedent( """ { @@ -1184,6 +1201,7 @@ def test_payment_context_without_any_payment_attempts(self): "objecttype_version": 1, "productaanvraag_type": "-dummy-", "update_existing_object": False, + "auth_attribute_path": [], "content_json": """{"amount": {{ payment.amount }}}""", } handler = ObjectsAPIV1Handler() diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_backend_v2.py b/src/openforms/registrations/contrib/objects_api/tests/test_backend_v2.py index 4eadb72bdb..d1a163e5f3 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_backend_v2.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_backend_v2.py @@ -87,6 +87,7 @@ def test_submission_with_objects_api_v2(self): "objecttype_version": 3, "upload_submission_csv": True, "update_existing_object": False, + "auth_attribute_path": [], "informatieobjecttype_submission_report": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/7a474713-0833-402a-8441-e467c08ac55b", "informatieobjecttype_submission_csv": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/b2d83b94-9b9b-4e80-a82f-73ff993c62f3", "informatieobjecttype_attachment": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/531f6c1a-97f7-478c-85f0-67d2f23661c7", @@ -193,6 +194,7 @@ def test_submission_with_objects_api_v2_with_payment_attributes(self): "objecttype_version": 3, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "payment_completed", @@ -293,6 +295,7 @@ def test_submission_with_file_components(self): "objecttype_version": 1, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "informatieobjecttype_attachment": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/531f6c1a-97f7-478c-85f0-67d2f23661c7", "organisatie_rsin": "000000000", "variables_mapping": [ @@ -369,6 +372,7 @@ def test_submission_with_file_components_container_variable(self): "objecttype_version": 1, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "informatieobjecttype_attachment": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/531f6c1a-97f7-478c-85f0-67d2f23661c7", "organisatie_rsin": "000000000", "variables_mapping": [ @@ -415,6 +419,7 @@ def test_submission_with_empty_optional_file(self): "objecttype_version": 1, "upload_submission_csv": False, "update_existing_object": False, + "auth_attribute_path": [], "informatieobjecttype_attachment": "http://localhost:8003/catalogi/api/v1/informatieobjecttypen/531f6c1a-97f7-478c-85f0-67d2f23661c7", "organisatie_rsin": "000000000", "variables_mapping": [ @@ -475,6 +480,7 @@ def test_addressNl_legacy_before_of_30(self): "objecttype": "8faed0fa-7864-4409-aa6d-533a37616a9e", "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "addressNl", @@ -549,6 +555,7 @@ def test_submission_with_map_component_inside_data(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "location", @@ -596,6 +603,7 @@ def test_layout_components(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "fieldset.textfield", @@ -639,6 +647,7 @@ def test_hidden_component(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "textfield", @@ -670,6 +679,7 @@ def test_public_reference_available(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "public_reference", @@ -716,6 +726,7 @@ def test_cosign_info_available(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "cosign_data", @@ -778,6 +789,7 @@ def test_cosign_info_not_available(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "cosign_date", @@ -833,6 +845,7 @@ def test_cosign_info_no_cosign_date(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "cosign_date", @@ -872,6 +885,7 @@ def test_auth_context_data_info_available(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "auth_context", @@ -983,6 +997,7 @@ def test_auth_context_data_info_parts_missing_variants(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "auth_context", @@ -1142,6 +1157,7 @@ def test_addressNl_with_specific_target_paths(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "addressNl", @@ -1208,6 +1224,7 @@ def test_addressNl_with_specific_target_paths_mapped_and_empty_submitted_data( "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ { "variable_key": "addressNl", @@ -1270,6 +1287,7 @@ def test_addressNl_with_object_target_path(self): "objecttype": UUID("f3f1b370-97ed-4730-bc7e-ebb20c230377"), "objecttype_version": 1, "update_existing_object": False, + "auth_attribute_path": [], "variables_mapping": [ {"variable_key": "addressNl", "target_path": ["addressNL"]} ], diff --git a/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py b/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py index de84b30108..1d613f5e9b 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_serializer.py @@ -200,7 +200,9 @@ def test_auth_attribute_path_required_if_update_existing_object_is_true(self): }, ) - self.assertFalse(options.is_valid()) + result = options.is_valid() + + self.assertFalse(result) self.assertIn("auth_attribute_path", options.errors) error = options.errors["auth_attribute_path"][0] self.assertEqual(error.code, "required") From e7739413b040a0b3f230df75d8ebd7b0eee12b8e Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 18:13:53 +0100 Subject: [PATCH 38/39] :art: [#4398] Refactor registration plugin tests * Converted the tests to make use of VCR for the actual implementation details of the ownership verification * Simplified the tests and ensured they're unit tests focused around a single method. We have a generic pattern test that ensures the method gets called. --- ...ts.test_ownership_check_does_not_pass.yaml | 154 ++++++++++++ ...heckTests.test_ownership_check_passes.yaml | 106 ++++++++ .../test_initial_data_ownership_validation.py | 237 ++++++++---------- 3 files changed, 367 insertions(+), 130 deletions(-) create mode 100644 src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_does_not_pass.yaml create mode 100644 src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_passes.yaml diff --git a/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_does_not_pass.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_does_not_pass.yaml new file mode 100644 index 0000000000..a7e8391b6c --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_does_not_pass.yaml @@ -0,0 +1,154 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, + "startAt": "2024-12-04"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '205' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8","uuid":"6cc052d2-f859-4fcd-a094-01274547e4f8","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-04","endAt":null,"registrationAt":"2024-12-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 04 Dec 2024 17:12:25 GMT + Location: + - http://localhost:8002/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8 + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8","uuid":"6cc052d2-f859-4fcd-a094-01274547e4f8","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-04","endAt":null,"registrationAt":"2024-12-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 04 Dec 2024 17:12:25 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8 + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/6cc052d2-f859-4fcd-a094-01274547e4f8","uuid":"6cc052d2-f859-4fcd-a094-01274547e4f8","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-04","endAt":null,"registrationAt":"2024-12-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 04 Dec 2024 17:12:25 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - 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/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_passes.yaml b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_passes.yaml new file mode 100644 index 0000000000..d44fef75a9 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/tests/files/vcr_cassettes/DataOwnershipCheckTests/DataOwnershipCheckTests.test_ownership_check_passes.yaml @@ -0,0 +1,106 @@ +interactions: +- request: + body: '{"type": "http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e", + "record": {"typeVersion": 1, "data": {"bsn": "111222333", "some": {"path": "foo"}}, + "startAt": "2024-12-04"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '205' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.2 + method: POST + uri: http://localhost:8002/api/v2/objects + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae","uuid":"cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-04","endAt":null,"registrationAt":"2024-12-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, POST, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 04 Dec 2024 17:12:25 GMT + Location: + - http://localhost:8002/api/v2/objects/cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - origin + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Authorization: + - Token 7657474c3d75f56ae0abd0d1bf7994b09964dca9 + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + User-Agent: + - python-requests/2.32.2 + method: GET + uri: http://localhost:8002/api/v2/objects/cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae + response: + body: + string: '{"url":"http://objects-web:8000/api/v2/objects/cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae","uuid":"cf334c6a-5a20-49fb-8ce5-e9c7b7a6c4ae","type":"http://objecttypes-web:8000/api/v2/objecttypes/8faed0fa-7864-4409-aa6d-533a37616a9e","record":{"index":1,"typeVersion":1,"data":{"bsn":"111222333","some":{"path":"foo"}},"geometry":null,"startAt":"2024-12-04","endAt":null,"registrationAt":"2024-12-04","correctionFor":null,"correctedBy":null}}' + headers: + Allow: + - GET, PUT, PATCH, DELETE, HEAD, OPTIONS + Connection: + - keep-alive + Content-Crs: + - EPSG:4326 + Content-Length: + - '432' + Content-Type: + - application/json + Cross-Origin-Opener-Policy: + - same-origin + Date: + - Wed, 04 Dec 2024 17:12:26 GMT + Referrer-Policy: + - same-origin + Server: + - nginx/1.27.3 + Vary: + - 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_initial_data_ownership_validation.py b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py index 729ecd4ebc..c077999786 100644 --- a/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py +++ b/src/openforms/registrations/contrib/objects_api/tests/test_initial_data_ownership_validation.py @@ -1,153 +1,130 @@ -from unittest.mock import patch +from pathlib import Path +from uuid import UUID from django.core.exceptions import PermissionDenied from django.test import TestCase, tag +from freezegun import freeze_time + +from openforms.authentication.service import AuthAttribute +from openforms.contrib.objects_api.clients import get_objects_client +from openforms.contrib.objects_api.helpers import prepare_data_for_registration from openforms.contrib.objects_api.tests.factories import ObjectsAPIGroupConfigFactory -from openforms.forms.tests.factories import FormFactory, FormRegistrationBackendFactory -from openforms.logging.models import TimelineLogProxy -from openforms.submissions.constants import PostSubmissionEvents -from openforms.submissions.tasks.registration import pre_registration from openforms.submissions.tests.factories import SubmissionFactory +from openforms.utils.tests.vcr import OFVCRMixin +from ..plugin import PLUGIN_IDENTIFIER, ObjectsAPIRegistration +from ..typing import RegistrationOptionsV2 -@tag("gh-4398") -class ObjectsAPIPrefillDataOwnershipCheckTests(TestCase): - def setUp(self): - super().setUp() - - self.objects_api_group_used = ObjectsAPIGroupConfigFactory.create( - for_test_docker_compose=True - ) - self.objects_api_group_unused = ObjectsAPIGroupConfigFactory.create() - - self.form = FormFactory.create() - - # An objects API backend with a different API group - FormRegistrationBackendFactory.create( - form=self.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_unused.pk, - "objecttype_version": 1, - "auth_attribute_path": ["bsn"], - }, - ) - # Another backend that should be ignored - FormRegistrationBackendFactory.create(form=self.form, backend="email") - # The backend that should be used to perform the check - self.backend = FormRegistrationBackendFactory.create( - form=self.form, - backend="objects_api", - options={ - "version": 2, - "objecttype": "3edfdaf7-f469-470b-a391-bb7ea015bd6f", - "objects_api_group": self.objects_api_group_used.pk, - "objecttype_version": 1, - "auth_attribute_path": ["nested", "bsn"], - }, - ) - - def test_verify_initial_data_ownership_not_called_if_initial_data_reference_missing( - self, - ): - submission = SubmissionFactory.create( - form=self.form, - completed_not_preregistered=True, - ) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: - pre_registration(submission.id, PostSubmissionEvents.on_completion) +@tag("gh-4398") +class DataOwnershipCheckTests(OFVCRMixin, TestCase): + VCR_TEST_FILES = Path(__file__).parent / "files" - mock_validate_object_ownership.assert_not_called() + @classmethod + def setUpTestData(cls): + super().setUpTestData() - def test_verify_initial_data_ownership_called_if_initial_data_reference_specified( - self, - ): - submission = SubmissionFactory.create( - form=self.form, - completed_not_preregistered=True, - initial_data_reference="1234", - finalised_registration_backend_key=self.backend.key, + cls.api_group = ObjectsAPIGroupConfigFactory.create( + for_test_docker_compose=True ) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership" - ) as mock_validate_object_ownership: - pre_registration(submission.id, PostSubmissionEvents.on_completion) - - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission) - self.assertEqual( - call.args[1].base_url, - self.objects_api_group_used.objects_service.api_root, + def test_ownership_check_passes(self): + # We manually create the objects instance as if it was created upfront by some + # external party + with ( + freeze_time("2024-12-04T18:11:00+01:00"), + get_objects_client(self.api_group) as client, + ): + object_data = client.create_object( + record_data=prepare_data_for_registration( + data={"bsn": "111222333", "some": {"path": "foo"}}, + objecttype_version=1, + ), + objecttype_url=( + "http://objecttypes-web:8000/api/v2/objecttypes/" + "8faed0fa-7864-4409-aa6d-533a37616a9e" + ), ) - self.assertEqual(call.args[2], ["nested", "bsn"]) - def test_verify_initial_data_ownership_raising_error_causes_failing_pre_registration( - self, - ): submission = SubmissionFactory.create( - form=self.form, completed_not_preregistered=True, - initial_data_reference="1234", + initial_data_reference=object_data["uuid"], + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, ) + plugin = ObjectsAPIRegistration(PLUGIN_IDENTIFIER) + options: RegistrationOptionsV2 = { + "objects_api_group": self.api_group, + "version": 2, + "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), + "objecttype_version": 1, + "update_existing_object": True, + "auth_attribute_path": ["bsn"], + "variables_mapping": [], + "iot_submission_report": "", + "iot_submission_csv": "", + "iot_attachment": "", + } + + result = plugin.verify_initial_data_ownership(submission, options) + + # if it doesn't pass, it raises PermissionDenied error instead + self.assertIsNone(result) + + def test_ownership_check_does_not_pass(self): + # We manually create the objects instance as if it was created upfront by some + # external party + with ( + freeze_time("2024-12-04T18:11:00+01:00"), + get_objects_client(self.api_group) as client, + ): + object_data = client.create_object( + record_data=prepare_data_for_registration( + data={"bsn": "111222333", "some": {"path": "foo"}}, + objecttype_version=1, + ), + objecttype_url=( + "http://objecttypes-web:8000/api/v2/objecttypes/" + "8faed0fa-7864-4409-aa6d-533a37616a9e" + ), + ) + plugin = ObjectsAPIRegistration(PLUGIN_IDENTIFIER) + options: RegistrationOptionsV2 = { + "objects_api_group": self.api_group, + "version": 2, + "objecttype": UUID("8faed0fa-7864-4409-aa6d-533a37616a9e"), + "objecttype_version": 1, + "update_existing_object": True, + "auth_attribute_path": ["bsn"], + "variables_mapping": [], + "iot_submission_report": "", + "iot_submission_csv": "", + "iot_attachment": "", + } + + with self.subTest("other BSN used"): + submission2 = SubmissionFactory.create( + completed_not_preregistered=True, + initial_data_reference=object_data["uuid"], + auth_info__value="999999999", + auth_info__attribute=AuthAttribute.bsn, + ) - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", - side_effect=PermissionDenied, - ) as mock_validate_object_ownership: with self.assertRaises(PermissionDenied): - pre_registration(submission.id, PostSubmissionEvents.on_completion) - self.assertEqual(mock_validate_object_ownership.call_count, 1) - - # Cannot compare with `.assert_has_calls`, because the client objects - # won't match - call = mock_validate_object_ownership.mock_calls[0] - - self.assertEqual(call.args[0], submission) - self.assertEqual( - call.args[1].base_url, - self.objects_api_group_unused.objects_service.api_root, + plugin.verify_initial_data_ownership(submission2, options) + + with self.subTest("wrong auth attribute path used"): + submission2 = SubmissionFactory.create( + completed_not_preregistered=True, + initial_data_reference=object_data["uuid"], + auth_info__value="111222333", + auth_info__attribute=AuthAttribute.bsn, ) - self.assertEqual(call.args[2], ["bsn"]) - - def test_verify_initial_data_ownership_missing_auth_attribute_path_causes_failing_pre_registration( - self, - ): - del self.backend.options["auth_attribute_path"] - self.backend.save() - - submission = SubmissionFactory.create( - form=self.form, - completed_not_preregistered=True, - initial_data_reference="1234", - finalised_registration_backend_key=self.backend.key, - ) + broken_options: RegistrationOptionsV2 = { + **options, + "auth_attribute_path": ["nested", "bsn"], + } - with patch( - "openforms.registrations.contrib.objects_api.plugin.validate_object_ownership", - ) as mock_validate_object_ownership: with self.assertRaises(PermissionDenied): - pre_registration(submission.id, PostSubmissionEvents.on_completion) - - # Not called, due to missing `auth_attribute_path` - self.assertEqual(mock_validate_object_ownership.call_count, 0) - - logs = TimelineLogProxy.objects.filter(object_id=submission.id) - self.assertEqual( - logs.filter( - extra_data__log_event="object_ownership_check_improperly_configured" - ).count(), - 1, - ) + plugin.verify_initial_data_ownership(submission2, broken_options) From 44c89d86336543c9ae433891f4e5221c3fb269dd Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 4 Dec 2024 18:18:13 +0100 Subject: [PATCH 39/39] :pencil: Update release checklist with VCR tasks --- .github/ISSUE_TEMPLATE/prepare-release.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/prepare-release.md b/.github/ISSUE_TEMPLATE/prepare-release.md index 4de09599db..b20f1a68d4 100644 --- a/.github/ISSUE_TEMPLATE/prepare-release.md +++ b/.github/ISSUE_TEMPLATE/prepare-release.md @@ -32,9 +32,9 @@ assignees: sergei-maertens - Payment plugins - [ ] Ogone legacy: `openforms.payments.contrib.ogone.tests.test_client` - Prefill - - [ ] Endpoints: `openforms.prefill.contrib.objects_api.tests.test_endpoints` - - [ ] Config: `openforms.prefill.contrib.objects_api.tests.test_config` - - [ ] Prefill: `openforms.prefill.contrib.objects_api.tests.test_prefill` + - [ ] Objects API: `openforms.prefill.contrib.objects_api` + - [ ] Suwinet: `openforms.prefill.contrib.suwinet` (testenv access has been retracted and won't + be reinstated) - Registration plugins: - [ ] Objects API: `openforms.registrations.contrib.objects_api` - [ ] ZGW APIs: `openforms.registrations.contrib.zgw_apis`