diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96f3a6814c..753f25e132 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,16 @@ Changelog .. note:: These release notes are in development! +**DigiD - Eherkenning Configuration** + +The configuration concerning the digid and eherkenning has been updated according to the +new version (0.9) of the django-digid-eherkenning library. The XML metadata file is now +automatically retrieved so you have to edit the configuration via: +**Admin** > **Configuratie** > **DigiD-configuratie** +**Admin** > **Configuratie** > **EHerkenning/eIDAS-configuratie** +and add the XML metadata url. The identity provider and the metadata file fields will be +automatically populated if the url is valid. + Upgrade procedure ----------------- diff --git a/requirements/base.txt b/requirements/base.txt index 676805278f..07cc98b6d6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -163,7 +163,7 @@ django-csp-reports==1.8.1 # via -r requirements/base.in django-decorator-include==3.0 # via -r requirements/base.in -django-digid-eherkenning==0.8.2 +django-digid-eherkenning==0.9.0 # via -r requirements/base.in django-filter==23.2 # via -r requirements/base.in diff --git a/requirements/ci.txt b/requirements/ci.txt index 6cfbc69950..1f8def20fa 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -264,7 +264,7 @@ django-decorator-include==3.0 # via # -c requirements/base.txt # -r requirements/base.txt -django-digid-eherkenning==0.8.2 +django-digid-eherkenning==0.9.0 # via # -c requirements/base.txt # -r requirements/base.txt diff --git a/requirements/dev.txt b/requirements/dev.txt index 541af6f679..7cae3c8042 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -295,7 +295,7 @@ django-decorator-include==3.0 # via # -c requirements/ci.txt # -r requirements/ci.txt -django-digid-eherkenning==0.8.2 +django-digid-eherkenning==0.9.0 # via # -c requirements/ci.txt # -r requirements/ci.txt diff --git a/requirements/extensions.txt b/requirements/extensions.txt index c30ffabd8e..e2c97f48de 100644 --- a/requirements/extensions.txt +++ b/requirements/extensions.txt @@ -230,7 +230,7 @@ django-decorator-include==3.0 # via # -c requirements/base.in # -r requirements/base.txt -django-digid-eherkenning==0.8.2 +django-digid-eherkenning==0.9.0 # via # -c requirements/base.in # -r requirements/base.txt diff --git a/src/openforms/conf/base.py b/src/openforms/conf/base.py index b02b4f3836..0af273f12e 100644 --- a/src/openforms/conf/base.py +++ b/src/openforms/conf/base.py @@ -210,6 +210,7 @@ "openforms.logging.apps.LoggingAppConfig", "openforms.contrib.bag.apps.BAGConfig", # TODO: remove once 2.4.0 is released "openforms.contrib.brp", + "openforms.contrib.digid_eherkenning", "openforms.contrib.haal_centraal", "openforms.contrib.kadaster", "openforms.contrib.kvk", @@ -1099,6 +1100,8 @@ "'self'", ] + config("CSP_EXTRA_DEFAULT_SRC", default=[], split=True) +CSP_FORM_ACTION = ("'self'",) + # * service.pdok.nl serves the tiles for the Leaflet maps (PNGs) and must be whitelisted # * the data: URIs are used by Leaflet (invisible pixel for memory management/image unloading) # and the signature component which saves the image drawn on the canvas as data: URI diff --git a/src/openforms/config/admin.py b/src/openforms/config/admin.py index 368b396683..ca461601c1 100644 --- a/src/openforms/config/admin.py +++ b/src/openforms/config/admin.py @@ -1,4 +1,6 @@ from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin @@ -182,9 +184,11 @@ class RichTextColorAdmin(admin.ModelAdmin): @admin.register(CSPSetting) class CSPSettingAdmin(admin.ModelAdmin): + readonly_fields = ("content_type_link",) fields = [ "directive", "value", + "content_type_link", ] list_display = [ "directive", @@ -197,3 +201,11 @@ class CSPSettingAdmin(admin.ModelAdmin): "directive", "value", ] + + def content_type_link(self, obj): + ct = obj.content_type + url = reverse(f"admin:{ct.app_label}_{ct.model}_change", args=(obj.object_id,)) + link = format_html('{t}', u=url, t=str(obj.content_object)) + return link + + content_type_link.short_description = _("Content type") diff --git a/src/openforms/config/migrations/0058_auto_20231026_1525.py b/src/openforms/config/migrations/0058_auto_20231026_1525.py new file mode 100644 index 0000000000..22fb1191bb --- /dev/null +++ b/src/openforms/config/migrations/0058_auto_20231026_1525.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.21 on 2023-10-26 13:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("config", "0057_globalconfiguration_recipients_email_digest"), + ] + + operations = [ + migrations.AddField( + model_name="cspsetting", + name="content_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="contenttypes.contenttype", + verbose_name="content type", + ), + ), + migrations.AddField( + model_name="cspsetting", + name="object_id", + field=models.TextField(blank=True, db_index=True, verbose_name="object id"), + ), + migrations.AlterField( + model_name="cspsetting", + name="value", + field=models.CharField( + help_text="CSP header value", max_length=255, verbose_name="value" + ), + ), + ] diff --git a/src/openforms/config/models.py b/src/openforms/config/models.py index d081043a48..b7d0775230 100644 --- a/src/openforms/config/models.py +++ b/src/openforms/config/models.py @@ -1,6 +1,10 @@ +import logging from collections import defaultdict from functools import partial +from django.contrib.admin.options import get_content_type_for_model +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.core.validators import ( FileExtensionValidator, @@ -8,7 +12,7 @@ MinValueValidator, RegexValidator, ) -from django.db import models +from django.db import models, transaction from django.template.loader import render_to_string from django.utils.encoding import force_str from django.utils.safestring import mark_safe @@ -32,6 +36,8 @@ from .constants import CSPDirective, UploadFileType from .utils import verify_clamav_connection +logger = logging.getLogger(__name__) + @ensure_default_language() def _render(filename): @@ -649,6 +655,26 @@ def as_dict(self): return {k: list(v) for k, v in ret.items()} +class CSPSettingManager(models.Manager.from_queryset(CSPSettingQuerySet)): + @transaction.atomic + def set_for( + self, obj: models.Model, settings: list[tuple[CSPDirective, str]] + ) -> None: + """ + Deletes all the connected csp settings and creates new ones based on the new provided data. + """ + instances = [ + CSPSetting(content_object=obj, directive=directive, value=value) + for directive, value in settings + ] + + CSPSetting.objects.filter( + content_type=get_content_type_for_model(obj), object_id=str(obj.id) + ).delete() + + self.bulk_create(instances) + + class CSPSetting(models.Model): directive = models.CharField( _("directive"), @@ -658,11 +684,24 @@ class CSPSetting(models.Model): ) value = models.CharField( _("value"), - max_length=128, + max_length=255, help_text=_("CSP header value"), ) + content_type = models.ForeignKey( + ContentType, + verbose_name=_("content type"), + on_delete=models.SET_NULL, + blank=True, + null=True, + ) + object_id = models.TextField( + verbose_name=_("object id"), + blank=True, + db_index=True, + ) + content_object = GenericForeignKey("content_type", "object_id") - objects = CSPSettingQuerySet.as_manager() + objects = CSPSettingManager() class Meta: ordering = ("directive", "value") diff --git a/src/openforms/config/tests/test_admin.py b/src/openforms/config/tests/test_admin.py new file mode 100644 index 0000000000..428f6c889e --- /dev/null +++ b/src/openforms/config/tests/test_admin.py @@ -0,0 +1,28 @@ +from django.contrib.admin.sites import AdminSite +from django.test import TestCase +from django.urls import reverse + +from openforms.config.models import CSPSetting +from openforms.payments.contrib.ogone.tests.factories import OgoneMerchantFactory + +from ..admin import CSPSettingAdmin + + +class TestCSPAdmin(TestCase): + def test_content_type_link(self): + OgoneMerchantFactory() + + csp = CSPSetting.objects.get() + + admin_site = AdminSite() + admin = CSPSettingAdmin(CSPSetting, admin_site) + + expected_url = reverse( + "admin:payments_ogone_ogonemerchant_change", + kwargs={"object_id": str(csp.object_id)}, + ) + expected_link = f'{str(csp.content_object)}' + + link = admin.content_type_link(csp) + + self.assertEqual(link, expected_link) diff --git a/src/openforms/contrib/digid_eherkenning/apps.py b/src/openforms/contrib/digid_eherkenning/apps.py new file mode 100644 index 0000000000..370d446507 --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class DigidEherkenningApp(AppConfig): + name = "openforms.contrib.digid_eherkenning" + label = "contrib_digid_eherkenning" + verbose_name = _("DigiD/Eherkenning utilities") + + def ready(self): + # register the signals + from .signals import trigger_csp_update # noqa diff --git a/src/openforms/contrib/digid_eherkenning/constants.py b/src/openforms/contrib/digid_eherkenning/constants.py new file mode 100644 index 0000000000..d3840d966f --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/constants.py @@ -0,0 +1,6 @@ +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration + +ADDITIONAL_CSP_VALUES = { + DigidConfiguration: "https://digid.nl https://*.digid.nl", + EherkenningConfiguration: "", +} diff --git a/src/openforms/contrib/digid_eherkenning/signals.py b/src/openforms/contrib/digid_eherkenning/signals.py new file mode 100644 index 0000000000..079fd8c447 --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/signals.py @@ -0,0 +1,14 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration + +from .utils import create_digid_eherkenning_csp_settings + + +@receiver(post_save, sender=DigidConfiguration) +@receiver(post_save, sender=EherkenningConfiguration) +def trigger_csp_update( + sender, instance: DigidConfiguration | EherkenningConfiguration, **kwargs +): + create_digid_eherkenning_csp_settings(instance) diff --git a/src/openforms/contrib/digid_eherkenning/tests/__init__.py b/src/openforms/contrib/digid_eherkenning/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/contrib/digid_eherkenning/tests/data/eherkenning-metadata.xml b/src/openforms/contrib/digid_eherkenning/tests/data/eherkenning-metadata.xml new file mode 100644 index 0000000000..34b0a18cea --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/tests/data/eherkenning-metadata.xml @@ -0,0 +1,97 @@ + + + + + + + + + + +mTSuC+50WptA9sO0wg/eFIyQtWofMlkbEYm5Zoj/GHo= + + + + + + + + + + + + + + urn:etoegang:core:assurance-class:loa4 + + + + + + + Test key 0 + + + + + + + + Test key 1 + + + + + + + + + + + urn:etoegang:1.9:EntityConcernedID:KvKnr + urn:etoegang:1.9:IntermediateEntityID:KvKnr + urn:etoegang:1.9:EntityConcernedID:Pseudo + urn:etoegang:1.9:EntityConcernedID:RSIN + urn:etoegang:1.9:IntermediateEntityID:RSIN + urn:etoegang:1.11:EntityConcernedID:eIDASLegalIdentifier + urn:etoegang:1.12:EntityConcernedID:BSN + urn:etoegang:1.12:EntityConcernedID:PseudoID + + + + + + + + Test key 2 + + + + + + + + Test key 3 + + + + + + + + + + + + + + + Test-iWelcome + Test-iWelcome + www.test-iwelcome.nl + + + support@test-iwelcome.nl + 000 222 111 + + diff --git a/src/openforms/contrib/digid_eherkenning/tests/data/metadata_POST_bindings.xml b/src/openforms/contrib/digid_eherkenning/tests/data/metadata_POST_bindings.xml new file mode 100644 index 0000000000..6ccc6a4ad8 --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/tests/data/metadata_POST_bindings.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + zIJH9lctgfbY1SLzbOZhOo2FN/qSqDi20MTd2OYN+qs= + + + + + + + Test key 0 + + + + + + Test key 1 + + + + + + + + + + Test key 2 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/openforms/contrib/digid_eherkenning/tests/test_csp_update.py b/src/openforms/contrib/digid_eherkenning/tests/test_csp_update.py new file mode 100644 index 0000000000..c368a707b6 --- /dev/null +++ b/src/openforms/contrib/digid_eherkenning/tests/test_csp_update.py @@ -0,0 +1,235 @@ +from pathlib import Path +from unittest.mock import patch + +from django.test import TestCase, override_settings +from django.urls import reverse + +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration +from privates.test import temp_private_root +from simple_certmanager.test.factories import CertificateFactory + +from openforms.config.constants import CSPDirective +from openforms.config.models import CSPSetting +from openforms.forms.tests.factories import ( + FormDefinitionFactory, + FormFactory, + FormStepFactory, +) +from openforms.utils.tests.cache import clear_caches + +TEST_FILES = Path(__file__).parent / "data" +DIGID_METADATA_POST = TEST_FILES / "metadata_POST_bindings.xml" +EHERKENNING_METADATA_POST = TEST_FILES / "eherkenning-metadata.xml" + + +@temp_private_root() +@override_settings(CORS_ALLOW_ALL_ORIGINS=True, IS_HTTPS=True) +class DigidCSPUpdateTests(TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cert = CertificateFactory.create(label="DigiD", with_private_key=True) + + cls.config = DigidConfiguration.get_solo() + + cls.config.certificate = cert + cls.config.idp_service_entity_id = "https://test-digid.nl" + cls.config.want_assertions_signed = False + cls.config.entity_id = "https://test-sp.nl" + cls.config.base_url = "https://test-sp.nl" + cls.config.service_name = "Test" + cls.config.service_description = "Test description" + cls.config.slo = False + cls.config.save() + + def setUp(self): + super().setUp() + + clear_caches() + self.addCleanup(clear_caches) + + @patch( + "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata" + ) + def test_csp_updates_for_digid(self, get_metadata): + # assert no csp entries exist with initial solo model + self.assertTrue(CSPSetting.objects.none) + + # assert new csp entry is added after adding metadata url + metadata_content = DIGID_METADATA_POST.read_text("utf-8") + get_metadata.return_value = metadata_content + self.config.metadata_file_source = "https://test-digid.nl" + self.config.save() + + csp_added = CSPSetting.objects.get() + + self.assertEqual(csp_added.content_object, self.config) + self.assertEqual(csp_added.directive, CSPDirective.FORM_ACTION) + self.assertEqual( + csp_added.value, + "https://test-digid.nl/saml/idp/request_authentication " + "https://test-digid.nl/saml/idp/request_logout " + "https://digid.nl " + "https://*.digid.nl", + ) + + # assert new csp entry is added and the old one is deleted after url update + self.config.metadata_file_source = "https://test-digid-post-bindings.nl" + self.config.save() + + csp_updated = CSPSetting.objects.get() + + self.assertEqual(get_metadata.call_count, 4) + self.assertFalse(CSPSetting.objects.filter(id=csp_added.id)) + self.assertEqual(csp_updated.content_object, self.config) + self.assertEqual(csp_updated.directive, CSPDirective.FORM_ACTION) + self.assertEqual( + csp_updated.value, + "https://test-digid.nl/saml/idp/request_authentication " + "https://test-digid.nl/saml/idp/request_logout " + "https://digid.nl " + "https://*.digid.nl", + ) + + @patch( + "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata" + ) + def test_response_headers_contain_form_action_values_in_digid(self, get_metadata): + form = FormFactory.create(authentication_backends=["digid"]) + form_definition = FormDefinitionFactory.create(login_required=True) + FormStepFactory.create(form_definition=form_definition, form=form) + + metadata_content = DIGID_METADATA_POST.read_text("utf-8") + get_metadata.return_value = metadata_content + self.config.metadata_file_source = "https://test-digid.nl" + self.config.save() + + login_url = reverse( + "authentication:start", kwargs={"slug": form.slug, "plugin_id": "digid"} + ) + form_path = reverse("core:form-detail", kwargs={"slug": form.slug}) + form_url = f"http://testserver{form_path}?_start=1" + + # redirect_to_digid_login + response = self.client.get(login_url, {"next": form_url}, follow=True) + + self.assertEqual(get_metadata.call_count, 2) + self.assertIn( + "form-action " + "'self' " + "https://test-digid.nl/saml/idp/request_authentication " + "https://test-digid.nl/saml/idp/request_logout " + "https://digid.nl " + "https://*.digid.nl;", + response.headers["Content-Security-Policy"], + ) + + +@temp_private_root() +class EherkenningCSPUpdateTests(TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cert = CertificateFactory.create(label="eHerkenning", with_private_key=True) + + cls.config = EherkenningConfiguration.get_solo() + + cls.config.certificate = cert + cls.config.idp_service_entity_id = ( + "urn:etoegang:DV:00000001111111111000:entities:9000" + ) + cls.config.want_assertions_signed = False + cls.config.entity_id = "https://test-sp.nl" + cls.config.base_url = "https://test-sp.nl" + cls.config.service_name = "Test" + cls.config.service_description = "Test" + cls.config.loa = "urn:etoegang:core:assurance-class:loa3" + cls.config.oin = "00000001111111111000" + cls.config.no_eidas = True + cls.config.privacy_policy = "https://test-sp.nl/privacy_policy" + cls.config.makelaar_id = "00000002222222222000" + cls.config.organization_name = "Test Organisation" + cls.config.save() + + def setUp(self): + super().setUp() + + clear_caches() + self.addCleanup(clear_caches) + + @patch( + "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata" + ) + def test_csp_updates_for_eherkenning(self, get_metadata): + # assert no csp entries exist with initial solo model + self.assertTrue(CSPSetting.objects.none) + + # assert new csp entry is added after adding metadata url + + metadata_content = EHERKENNING_METADATA_POST.read_text("utf-8") + get_metadata.return_value = metadata_content + self.config.metadata_file_source = "https://test-sp.nl" + self.config.save() + + csp_added = CSPSetting.objects.get() + + self.assertEqual(csp_added.directive, CSPDirective.FORM_ACTION) + self.assertEqual( + csp_added.value, + "https://test-iwelcome.nl/broker/sso/1.13 " + "https://ehm01.iwelcome.nl/broker/slo/1.13", + ) + + # assert new csp entry is added and old one is deleted after url update + self.config.metadata_file_source = "https://updated-test-sp.nl" + self.config.save() + + csp_updated = CSPSetting.objects.get() + + self.assertEqual(get_metadata.call_count, 4) + self.assertFalse(CSPSetting.objects.filter(id=csp_added.id)) + self.assertEqual(csp_added.directive, CSPDirective.FORM_ACTION) + self.assertEqual( + csp_updated.value, + "https://test-iwelcome.nl/broker/sso/1.13 " + "https://ehm01.iwelcome.nl/broker/slo/1.13", + ) + + @patch( + "onelogin.saml2.idp_metadata_parser.OneLogin_Saml2_IdPMetadataParser.get_metadata" + ) + def test_response_headers_contain_form_action_values_in_eherkenning( + self, get_metadata + ): + form = FormFactory.create( + authentication_backends=["eherkenning"], + generate_minimal_setup=True, + formstep__form_definition__login_required=True, + ) + metadata_content = EHERKENNING_METADATA_POST.read_text("utf-8") + get_metadata.return_value = metadata_content + self.config.metadata_file_source = ( + "urn:etoegang:DV:00000001111111111000:entities:9000" + ) + self.config.save() + + login_url = reverse( + "authentication:start", + kwargs={"slug": form.slug, "plugin_id": "eherkenning"}, + ) + form_path = reverse("core:form-detail", kwargs={"slug": form.slug}) + form_url = f"http://testserver{form_path}" + + # redirect_to_eherkenning_login + response = self.client.get(login_url, {"next": form_url}, follow=True) + + self.assertEqual(get_metadata.call_count, 2) + self.assertIn( + "form-action " + "'self' " + "https://test-iwelcome.nl/broker/sso/1.13 " + "https://ehm01.iwelcome.nl/broker/slo/1.13;", + response.headers["Content-Security-Policy"], + ) diff --git a/src/openforms/contrib/digid_eherkenning/utils.py b/src/openforms/contrib/digid_eherkenning/utils.py index 42c3d403db..809ffc0c5d 100644 --- a/src/openforms/contrib/digid_eherkenning/utils.py +++ b/src/openforms/contrib/digid_eherkenning/utils.py @@ -2,7 +2,13 @@ from django.templatetags.static import static +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration + from openforms.authentication.constants import LogoAppearance +from openforms.config.constants import CSPDirective +from openforms.config.models import CSPSetting + +from .constants import ADDITIONAL_CSP_VALUES def get_digid_logo(request) -> Dict[str, str]: @@ -19,3 +25,20 @@ def get_eherkenning_logo(request) -> Dict[str, str]: "href": "https://www.eherkenning.nl/", "appearance": LogoAppearance.light, } + + +def create_digid_eherkenning_csp_settings( + config: DigidConfiguration | EherkenningConfiguration, +) -> None: + if not config.metadata_file_source: + return + + # create the new directives based on the POST bindings of the metadata XML and + # the additional constants + urls, _ = config.process_metadata_from_xml_source() + csp_values = [urls["sso_url"], urls["slo_url"]] + if additional_csp_values := ADDITIONAL_CSP_VALUES.get(type(config), []): + csp_values.append(additional_csp_values) + + form_action_urls = " ".join(csp_values) + CSPSetting.objects.set_for(config, [(CSPDirective.FORM_ACTION, form_action_urls)]) diff --git a/src/openforms/payments/contrib/ogone/models.py b/src/openforms/payments/contrib/ogone/models.py index c20f37d055..31d394f4f5 100644 --- a/src/openforms/payments/contrib/ogone/models.py +++ b/src/openforms/payments/contrib/ogone/models.py @@ -2,6 +2,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from openforms.config.constants import CSPDirective + from .constants import HashAlgorithm, OgoneEndpoints @@ -61,3 +63,10 @@ def clean(self): def __str__(self): return self.label + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + from openforms.config.models import CSPSetting + + CSPSetting.objects.set_for(self, [(CSPDirective.FORM_ACTION, self.endpoint)]) diff --git a/src/openforms/payments/contrib/ogone/tests/test_models.py b/src/openforms/payments/contrib/ogone/tests/test_models.py new file mode 100644 index 0000000000..37d54f2724 --- /dev/null +++ b/src/openforms/payments/contrib/ogone/tests/test_models.py @@ -0,0 +1,81 @@ +from django.test import TestCase + +from openforms.config.constants import CSPDirective +from openforms.config.models import CSPSetting + +from ..constants import OgoneEndpoints +from .factories import OgoneMerchantFactory + + +class CSPUpdateTests(TestCase): + def test_csp_is_saved_for_new_merchant_and_url_preset(self): + self.assertTrue(CSPSetting.objects.none) + + merchant = OgoneMerchantFactory() + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, OgoneEndpoints.test) + + def test_csp_is_saved_for_new_merchant_and_custom_url(self): + self.assertTrue(CSPSetting.objects.none) + + merchant = OgoneMerchantFactory(endpoint_custom="http://example.com") + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, "http://example.com") + + def test_csp_is_updated_for_existing_merchant_and_url_preset(self): + self.assertTrue(CSPSetting.objects.none) + + # initial values + merchant = OgoneMerchantFactory() + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, OgoneEndpoints.test) + + # updated values + merchant.endpoint_preset = OgoneEndpoints.live + merchant.save() + + # assert the original value has been deleted + self.assertEqual(CSPSetting.objects.count(), 1) + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, OgoneEndpoints.live) + + def test_csp_is_updated_for_existing_merchant_and_custom_url(self): + self.assertTrue(CSPSetting.objects.none) + + # initial values + merchant = OgoneMerchantFactory(endpoint_custom="http://example.com") + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, "http://example.com") + + # updated values + merchant.endpoint_preset = OgoneEndpoints.live + merchant.save() + + # assert the original value has been deleted + self.assertEqual(CSPSetting.objects.count(), 1) + + csp = CSPSetting.objects.get() + + self.assertEqual(csp.content_object, merchant) + self.assertEqual(csp.directive, CSPDirective.FORM_ACTION) + self.assertEqual(csp.value, "http://example.com") diff --git a/src/openforms/payments/contrib/ogone/tests/test_plugin.py b/src/openforms/payments/contrib/ogone/tests/test_plugin.py index a1be7a892c..75b3d705c8 100644 --- a/src/openforms/payments/contrib/ogone/tests/test_plugin.py +++ b/src/openforms/payments/contrib/ogone/tests/test_plugin.py @@ -8,7 +8,7 @@ from ....registry import register from ....tests.factories import SubmissionPaymentFactory -from ..constants import OgoneStatus, PaymentStatus +from ..constants import OgoneEndpoints, OgoneStatus, PaymentStatus from ..plugin import RETURN_ACTION_PARAM from ..signing import calculate_sha_out from .factories import OgoneMerchantFactory @@ -158,6 +158,12 @@ def test_webhook(self): self.assertEqual(response.status_code, 200) + # assert created csp header is in the response as well + self.assertIn( + f"form-action 'self' {OgoneEndpoints.test};", + response.headers["Content-Security-Policy"], + ) + submission.refresh_from_db() payment.refresh_from_db() self.assertEqual(payment.status, PaymentStatus.completed)