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`
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,
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/authentication/views.py b/src/openforms/authentication/views.py
index bd298e8acb..d1cb20bbd9 100644
--- a/src/openforms/authentication/views.py
+++ b/src/openforms/authentication/views.py
@@ -338,7 +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)
-
return response
def _handle_co_sign(self, form: Form, plugin: BasePlugin) -> None:
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
new file mode 100644
index 0000000000..ce7269b618
--- /dev/null
+++ b/src/openforms/contrib/objects_api/ownership_validation.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import logging
+
+from django.core.exceptions import PermissionDenied
+
+from glom import Path, PathAccessError, glom
+from requests.exceptions import RequestException
+
+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__)
+
+
+def validate_object_ownership(
+ submission: Submission,
+ client: ObjectsClient,
+ object_attribute: list[str],
+ plugin: BasePrefillPlugin | BaseRegistrationPlugin,
+) -> 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
+
+ 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 as e:
+ logger.exception(
+ "Something went wrong while trying to retrieve "
+ "object for initial_data_reference"
+ )
+ raise PermissionDenied from e
+
+ 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"
+ )
+
+ 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,
+ )
+ 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/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
new file mode 100644
index 0000000000..955b212c8a
--- /dev/null
+++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_backend_without_options_does_not_raise_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/0122126f-4a7f-49d4-b131-b83786e15acf
+ response:
+ body:
+ 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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '422'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:21:59 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_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
new file mode 100644
index 0000000000..955b212c8a
--- /dev/null
+++ b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/ObjectsAPIInitialDataOwnershipValidatorTests.test_no_backends_configured_does_not_raise_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/0122126f-4a7f-49d4-b131-b83786e15acf
+ response:
+ body:
+ 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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '422'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:21:59 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_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/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
new file mode 100644
index 0000000000..955b212c8a
--- /dev/null
+++ b/src/openforms/contrib/objects_api/tests/files/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/0122126f-4a7f-49d4-b131-b83786e15acf
+ response:
+ body:
+ 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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '422'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:21:59 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_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..45e5b03a94
--- /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-26"}}'
+ 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/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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '433'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:21:59 GMT
+ Location:
+ - http://localhost:8002/api/v2/objects/ebae7217-7a91-43bb-bfce-82c353e46289
+ 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/ebae7217-7a91-43bb-bfce-82c353e46289
+ response:
+ body:
+ 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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '433'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:22:00 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_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
new file mode 100644
index 0000000000..20c1a566aa
--- /dev/null
+++ b/src/openforms/contrib/objects_api/tests/files/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/0122126f-4a7f-49d4-b131-b83786e15acf
+ response:
+ body:
+ 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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '422'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:22:00 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/setUpTestData.yaml b/src/openforms/contrib/objects_api/tests/files/vcr_cassettes/ObjectsAPIInitialDataOwnershipValidatorTests/setUpTestData.yaml
new file mode 100644
index 0000000000..381d3cafcd
--- /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-11-26"}}'
+ 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/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
+ Connection:
+ - keep-alive
+ Content-Crs:
+ - EPSG:4326
+ Content-Length:
+ - '422'
+ Content-Type:
+ - application/json
+ Cross-Origin-Opener-Policy:
+ - same-origin
+ Date:
+ - Tue, 26 Nov 2024 13:21:59 GMT
+ Location:
+ - http://localhost:8002/api/v2/objects/0122126f-4a7f-49d4-b131-b83786e15acf
+ 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_ownership_validation.py b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py
new file mode 100644
index 0000000000..ba9ac0c643
--- /dev/null
+++ b/src/openforms/contrib/objects_api/tests/test_ownership_validation.py
@@ -0,0 +1,208 @@
+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 HTTPError, 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.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, with_setup_test_data_vcr
+
+from ..ownership_validation import validate_object_ownership
+
+TEST_FILES = (Path(__file__).parent / "files").resolve()
+
+
+PLUGIN = ObjectsAPIRegistration("test")
+
+
+@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
+ )
+
+ 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(
+ 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,
+ )
+
+ with get_objects_client(self.objects_api_group_used) as client:
+ 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)
+
+ logs = TimelineLogProxy.objects.for_object(submission)
+ self.assertEqual(
+ logs.filter_event("object_ownership_check_anonymous_user").count(), 1
+ )
+
+ @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,
+ )
+
+ 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.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):
+ 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,
+ )
+
+ 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):
+ 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,
+ )
+
+ 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(
+ "openforms.contrib.objects_api.clients.objects.ObjectsClient.get_object",
+ 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 fail
+ """
+ submission = SubmissionFactory.create(
+ auth_info__value="111222333",
+ auth_info__attribute=AuthAttribute.bsn,
+ initial_data_reference="irrelevant",
+ )
+ 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")
+ @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="irrelevant",
+ )
+
+ with get_objects_client(self.objects_api_group_used) as client:
+ with self.assertRaises(PermissionDenied):
+ validate_object_ownership(submission, client, ["bsn"], PLUGIN)
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/forms/tests/variables/test_viewset.py b/src/openforms/forms/tests/variables/test_viewset.py
index 8796f70483..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,7 +1052,7 @@ 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"},
}
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)
diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json
index 19f6fc0f02..790c4a6d3e 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,
@@ -1607,6 +1613,12 @@
"value": "Street name"
}
],
+ "DFQ0Pq": [
+ {
+ "type": 0,
+ "value": "Update existing objects"
+ }
+ ],
"DGpAyT": [
{
"type": 0,
@@ -5361,6 +5373,12 @@
"value": "Partner 1"
}
],
+ "lu7yMK": [
+ {
+ "type": 0,
+ "value": "Path to auth attribute (e.g. BSN/KVK) in objects"
+ }
+ ],
"m20av3": [
{
"type": 0,
@@ -6409,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 0c6c0e3eca..e354b6c13e 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,
@@ -1628,6 +1634,12 @@
"value": "Straatnaam"
}
],
+ "DFQ0Pq": [
+ {
+ "type": 0,
+ "value": "Update existing objects"
+ }
+ ],
"DGpAyT": [
{
"type": 0,
@@ -5383,6 +5395,12 @@
"value": "Partner 1"
}
],
+ "lu7yMK": [
+ {
+ "type": 0,
+ "value": "Path to auth attribute (e.g. BSN/KVK) in objects"
+ }
+ ],
"m20av3": [
{
"type": 0,
@@ -6431,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/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 54035a01e9..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(),
@@ -740,6 +750,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: 'Update existing objects (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.parentElement.parentElement).toHaveClass('field--disabled');
+
+ const updateExistingObject = modal.getByLabelText('Bestaand object bijwerken');
+ await userEvent.click(updateExistingObject);
+
+ // Checking `updateExistingObject` should make `authAttributePath` no longer disabled
+ expect(authAttributePath.parentElement.parentElement).not.toHaveClass('field--disabled');
+ }
+ );
+
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/AddressNlObjectsApiVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/AddressNlObjectsApiVariableConfigurationEditor.js
index 47c8b75a30..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,11 @@ 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 {TargetPathSelect} from 'components/admin/forms/objects_api';
import ErrorMessage from 'components/errors/ErrorMessage';
import {post} from 'utils/fetch';
-import {TargetPathDisplay} from './ObjectsApiVariableConfigurationEditor';
+import {MappedVariableTargetPathSelect} from './GenericObjectsApiVariableConfigurationEditor';
const ADDRESSNL_NESTED_PROPERTIES = {
postcode: {type: 'string'},
@@ -59,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,
@@ -91,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));
@@ -119,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 (
@@ -151,7 +150,7 @@ export const AddressNlEditor = ({
defaultMessage="Whether to map the specific subfield of addressNl component"
/>
}
- checked={isSpecificTargetPaths}
+ checked={specificTargetPaths}
onChange={onSpecificTargetPathsChange}
/>
@@ -165,18 +164,19 @@ export const AddressNlEditor = ({
description="'JSON Schema object target' label"
/>
}
- disabled={isSpecificTargetPaths}
+ disabled={specificTargetPaths}
>
-
- {isSpecificTargetPaths && (
+ {specificTargetPaths && (
)}
- {!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 (
- (
-