From 6cb949830525383cf3c1f7e9a06920dcfc56326b Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 10 Dec 2024 14:20:03 +0100 Subject: [PATCH 1/8] :arrow_up: [#200] Upgrade commonground-api-common to 2.2.0 --- requirements/base.in | 2 ++ requirements/base.txt | 41 ++++++++++++++++++++++++++---------- requirements/ci.txt | 48 +++++++++++++++++++++++++++---------------- requirements/dev.txt | 47 +++++++++++++++++++++++++++--------------- 4 files changed, 92 insertions(+), 46 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index eb5f3665..5edff61d 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,3 +4,5 @@ open-api-framework markdown self-certifi furl +zgw-consumers[setup-configuration] +commonground-api-common[setup-configuration] diff --git a/requirements/base.txt b/requirements/base.txt index 0fd5c195..ade0a606 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,11 +1,13 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # ./bin/compile_dependencies.sh # amqp==5.2.0 # via kombu +annotated-types==0.7.0 + # via pydantic ape-pie==0.2.0 # via # commonground-api-common @@ -18,8 +20,6 @@ asgiref==3.8.1 # django-cors-headers asn1crypto==1.5.1 # via webauthn -async-timeout==4.0.3 - # via redis attrs==24.2.0 # via # glom @@ -62,8 +62,10 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -commonground-api-common==2.1.0 - # via open-api-framework +commonground-api-common[setup-configuration]==2.2.0 + # via + # -r requirements/base.in + # open-api-framework coreapi==2.3.3 # via commonground-api-common coreschema==0.0.4 @@ -151,8 +153,11 @@ django-rest-framework-condition==0.1.1 # via commonground-api-common django-sendfile2==0.7.1 # via django-privates -django-setup-configuration==0.1.0 - # via open-api-framework +django-setup-configuration==0.4.0 + # via + # commonground-api-common + # open-api-framework + # zgw-consumers django-simple-certmanager==2.3.0 # via zgw-consumers django-solo==2.3.0 @@ -260,6 +265,16 @@ psycopg2==2.9.9 # via open-api-framework pycparser==2.22 # via cffi +pydantic==2.10.2 + # via + # django-setup-configuration + # pydantic-settings +pydantic-core==2.27.1 + # via pydantic +pydantic-settings[yaml]==2.6.1 + # via + # django-setup-configuration + # pydantic-settings pyjwt==2.9.0 # via # commonground-api-common @@ -277,7 +292,9 @@ python-dateutil==2.9.0.post0 python-decouple==3.8 # via open-api-framework python-dotenv==1.0.1 - # via open-api-framework + # via + # open-api-framework + # pydantic-settings pytz==2024.1 # via # drf-yasg @@ -287,6 +304,7 @@ pyyaml==6.0.2 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings qrcode==7.4.2 # via django-two-factor-auth redis==5.0.8 @@ -328,9 +346,9 @@ tornado==6.4.1 # via flower typing-extensions==4.12.2 # via - # asgiref - # django-solo # mozilla-django-oidc-db + # pydantic + # pydantic-core # qrcode # zgw-consumers tzdata==2024.1 @@ -360,8 +378,9 @@ webencodings==0.5.1 # via bleach wrapt==1.16.0 # via elastic-apm -zgw-consumers==0.35.1 +zgw-consumers[setup-configuration]==0.36.0 # via + # -r requirements/base.in # commonground-api-common # notifications-api-common # open-api-framework diff --git a/requirements/ci.txt b/requirements/ci.txt index 093b6678..9b8816ea 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # ./bin/compile_dependencies.sh @@ -10,6 +10,10 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +annotated-types==0.7.0 + # via + # -r requirements/base.txt + # pydantic ape-pie==0.2.0 # via # -r requirements/base.txt @@ -26,10 +30,6 @@ asn1crypto==1.5.1 # via # -r requirements/base.txt # webauthn -async-timeout==4.0.3 - # via - # -r requirements/base.txt - # redis attrs==24.2.0 # via # -r requirements/base.txt @@ -102,10 +102,11 @@ click-repl==0.3.0 # celery codecov==2.1.13 # via -r requirements/ci.in -commonground-api-common[testutils]==2.1.0 +commonground-api-common[setup-configuration,testutils]==2.2.0 # via # -r requirements/base.txt # -r requirements/test-tools.in + # commonground-api-common # open-api-framework commonmark==0.9.1 # via recommonmark @@ -241,10 +242,12 @@ django-sendfile2==0.7.1 # via # -r requirements/base.txt # django-privates -django-setup-configuration==0.1.0 +django-setup-configuration==0.4.0 # via # -r requirements/base.txt + # commonground-api-common # open-api-framework + # zgw-consumers django-simple-certmanager==2.3.0 # via # -r requirements/base.txt @@ -319,8 +322,6 @@ elastic-apm==6.23.0 # via # -r requirements/base.txt # open-api-framework -exceptiongroup==1.2.2 - # via pytest face==20.1.1 # via # -r requirements/base.txt @@ -475,6 +476,20 @@ pycparser==2.22 # via # -r requirements/base.txt # cffi +pydantic==2.10.2 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings +pydantic-core==2.27.1 + # via + # -r requirements/base.txt + # pydantic +pydantic-settings[yaml]==2.6.1 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings pyflakes==3.2.0 # via flake8 pygments==2.18.0 @@ -514,6 +529,7 @@ python-dotenv==1.0.1 # via # -r requirements/base.txt # open-api-framework + # pydantic-settings pytz==2024.1 # via # -r requirements/base.txt @@ -525,6 +541,7 @@ pyyaml==6.0.2 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings # vcrpy # zgw-consumers-oas qrcode==7.4.2 @@ -614,11 +631,6 @@ sqlparse==0.5.1 # django tblib==1.7.0 # via -r requirements/test-tools.in -tomli==2.0.1 - # via - # black - # pytest - # sphinx tornado==6.4.1 # via # -r requirements/base.txt @@ -626,10 +638,9 @@ tornado==6.4.1 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref - # black - # django-solo # mozilla-django-oidc-db + # pydantic + # pydantic-core # qrcode # zgw-consumers # zgw-consumers-oas @@ -686,11 +697,12 @@ wrapt==1.16.0 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.35.1 +zgw-consumers[setup-configuration]==0.36.0 # via # -r requirements/base.txt # commonground-api-common # notifications-api-common # open-api-framework + # zgw-consumers zgw-consumers-oas==1.0.0 # via commonground-api-common diff --git a/requirements/dev.txt b/requirements/dev.txt index 5cad684b..34a0d575 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # ./bin/compile_dependencies.sh @@ -10,6 +10,10 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +annotated-types==0.7.0 + # via + # -r requirements/base.txt + # pydantic ape-pie==0.2.0 # via # -r requirements/base.txt @@ -26,10 +30,6 @@ asn1crypto==1.5.1 # via # -r requirements/base.txt # webauthn -async-timeout==4.0.3 - # via - # -r requirements/base.txt - # redis attrs==24.2.0 # via # -r requirements/base.txt @@ -105,10 +105,11 @@ click-repl==0.3.0 # via # -r requirements/base.txt # celery -commonground-api-common[testutils]==2.1.0 +commonground-api-common[setup-configuration,testutils]==2.2.0 # via # -r requirements/base.txt # -r requirements/test-tools.in + # commonground-api-common # open-api-framework commonmark==0.9.1 # via recommonmark @@ -248,10 +249,12 @@ django-sendfile2==0.7.1 # via # -r requirements/base.txt # django-privates -django-setup-configuration==0.1.0 +django-setup-configuration==0.4.0 # via # -r requirements/base.txt + # commonground-api-common # open-api-framework + # zgw-consumers django-simple-certmanager==2.3.0 # via # -r requirements/base.txt @@ -482,6 +485,20 @@ pycparser==2.22 # via # -r requirements/base.txt # cffi +pydantic==2.10.2 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings +pydantic-core==2.27.1 + # via + # -r requirements/base.txt + # pydantic +pydantic-settings[yaml]==2.6.1 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings pyflakes==3.2.0 # via flake8 pygments==2.16.1 @@ -521,6 +538,7 @@ python-dotenv==1.0.1 # via # -r requirements/base.txt # open-api-framework + # pydantic-settings pytz==2024.1 # via # -r requirements/base.txt @@ -532,6 +550,7 @@ pyyaml==6.0.2 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings # vcrpy # zgw-consumers-oas qrcode==7.4.2 @@ -620,12 +639,6 @@ sqlparse==0.5.1 # django-debug-toolbar tblib==1.7.0 # via -r requirements/test-tools.in -tomli==2.0.1 - # via - # black - # build - # pip-tools - # pyproject-hooks tornado==6.4.1 # via # -r requirements/base.txt @@ -633,10 +646,9 @@ tornado==6.4.1 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref - # black - # django-solo # mozilla-django-oidc-db + # pydantic + # pydantic-core # qrcode # zgw-consumers # zgw-consumers-oas @@ -695,12 +707,13 @@ wrapt==1.16.0 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.35.1 +zgw-consumers[setup-configuration]==0.36.0 # via # -r requirements/base.txt # commonground-api-common # notifications-api-common # open-api-framework + # zgw-consumers zgw-consumers-oas==1.0.0 # via commonground-api-common From c9f679644a0360a72463eec921ef3d9c4c4ce53a Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 6 Dec 2024 11:29:04 +0100 Subject: [PATCH 2/8] :recycle: [#200] Configure Autorisaties API connection via setup-configuration several steps have been moved to their respective libraries --- src/nrc/conf/includes/base.py | 45 ++------ src/nrc/config/authorization.py | 150 +++------------------------ src/nrc/config/models.py | 20 ++++ src/nrc/config/notification_retry.py | 37 ------- src/nrc/config/site.py | 33 ++---- 5 files changed, 52 insertions(+), 233 deletions(-) create mode 100644 src/nrc/config/models.py delete mode 100644 src/nrc/config/notification_retry.py diff --git a/src/nrc/conf/includes/base.py b/src/nrc/conf/includes/base.py index fc9f4fd2..fcccbdf0 100644 --- a/src/nrc/conf/includes/base.py +++ b/src/nrc/conf/includes/base.py @@ -132,10 +132,13 @@ # Django setup configuration # SETUP_CONFIGURATION_STEPS = [ - "nrc.config.site.SiteConfigurationStep", + "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", + "vng_api_common.contrib.setup_configuration.steps.JWTSecretsConfigurationStep", "nrc.config.authorization.AuthorizationStep", - "nrc.config.authorization.OpenZaakAuthStep", - "nrc.config.notification_retry.NotificationRetryConfigurationStep", + # TODO these need to be implemented later + # "notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep", + # TODO this should be moved to `django-setup-configuration` + # "nrc.config.site.SiteConfigurationStep", ] # @@ -158,38 +161,4 @@ # Open Notificaties settings # -# Settings for setup_configuration command -# sites config -SITES_CONFIG_ENABLE = config("SITES_CONFIG_ENABLE", default=False, add_to_docs=False) -OPENNOTIFICATIES_DOMAIN = config("OPENNOTIFICATIES_DOMAIN", "", add_to_docs=False) -OPENNOTIFICATIES_ORGANIZATION = config( - "OPENNOTIFICATIES_ORGANIZATION", "", add_to_docs=False -) -# notif -> OZ auth config -AUTHORIZATION_CONFIG_ENABLE = config( - "AUTHORIZATION_CONFIG_ENABLE", default=False, add_to_docs=False -) -AUTORISATIES_API_ROOT = config("AUTORISATIES_API_ROOT", "", add_to_docs=False) -NOTIF_OPENZAAK_CLIENT_ID = config("NOTIF_OPENZAAK_CLIENT_ID", "", add_to_docs=False) -NOTIF_OPENZAAK_SECRET = config("NOTIF_OPENZAAK_SECRET", "", add_to_docs=False) -# OZ -> notif config -OPENZAAK_NOTIF_CONFIG_ENABLE = config( - "OPENZAAK_NOTIF_CONFIG_ENABLE", default=False, add_to_docs=False -) -OPENZAAK_NOTIF_CLIENT_ID = config("OPENZAAK_NOTIF_CLIENT_ID", "", add_to_docs=False) -OPENZAAK_NOTIF_SECRET = config("OPENZAAK_NOTIF_SECRET", "", add_to_docs=False) - -# setup configuration for Notification retry -# Retry settings for delivering notifications to subscriptions -NOTIFICATION_RETRY_CONFIG_ENABLE = config( - "NOTIFICATION_RETRY_CONFIG_ENABLE", default=False, add_to_docs=False -) -NOTIFICATION_DELIVERY_MAX_RETRIES = config( - "NOTIFICATION_DELIVERY_MAX_RETRIES", None, add_to_docs=False -) -NOTIFICATION_DELIVERY_RETRY_BACKOFF = config( - "NOTIFICATION_DELIVERY_RETRY_BACKOFF", None, add_to_docs=False -) -NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX = config( - "NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX", None, add_to_docs=False -) +OPENNOTIFICATIES_DOMAIN = config("OPENNOTIFICATIES_DOMAIN", "") diff --git a/src/nrc/config/authorization.py b/src/nrc/config/authorization.py index 0537f4c9..cea0d93b 100644 --- a/src/nrc/config/authorization.py +++ b/src/nrc/config/authorization.py @@ -1,38 +1,24 @@ # SPDX-License-Identifier: EUPL-1.2 # Copyright (C) 2022 Dimpact -from typing import Iterable - -from django.conf import settings -from django.urls import reverse - -import requests from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import SelfTestFailed from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes -from vng_api_common.authorizations.utils import generate_jwt -from vng_api_common.models import JWTSecret from zgw_consumers.models import Service -from nrc.utils import build_absolute_url - - -def _generate_service_slug(existing_slugs: Iterable[str]) -> str: - default_slug = "authorization-api-service" +from .models import AuthorizationsConfigModel - if not existing_slugs or default_slug not in existing_slugs: - return default_slug - slug = default_slug - count = 1 - - while slug in existing_slugs: - count += 1 - slug = f"{default_slug}-{count}" - - return slug +def get_service(slug: str) -> Service: + """ + Try to find a Service and re-raise DoesNotExist with the identifier to make debugging + easier + """ + try: + return Service.objects.get(slug=slug) + except Service.DoesNotExist as e: + raise Service.DoesNotExist(f"{str(e)} (identifier = {slug})") -class AuthorizationStep(BaseConfigurationStep): +class AuthorizationStep(BaseConfigurationStep[AuthorizationsConfigModel]): """ Open Notificaties uses Autorisaties API to check permissions of the clients. @@ -43,119 +29,17 @@ class AuthorizationStep(BaseConfigurationStep): If the client_id or secret is changed, run this command with 'overwrite' flag """ - verbose_name = "Authorization Configuration" - required_settings = [ - "AUTORISATIES_API_ROOT", - "NOTIF_OPENZAAK_CLIENT_ID", - "NOTIF_OPENZAAK_SECRET", - ] - enable_setting = "AUTHORIZATION_CONFIG_ENABLE" + verbose_name = "Configuration for Autorisaties API" + config_model = AuthorizationsConfigModel + namespace = "autorisaties_api" + enable_setting = "autorisaties_api_config_enable" - def is_configured(self) -> bool: - auth_config = AuthorizationsConfig.get_solo() - service = auth_config.authorizations_api_service - - if not service: - return False - - return service.api_root == settings.AUTORISATIES_API_ROOT - - def configure(self) -> None: - # Step 1 + def execute(self, model: AuthorizationsConfigModel) -> None: auth_config = AuthorizationsConfig.get_solo() if auth_config.component != ComponentTypes.nrc: auth_config.component = ComponentTypes.nrc - # Step 2 - organization = ( - settings.OPENNOTIFICATIES_ORGANIZATION or settings.NOTIF_OPENZAAK_CLIENT_ID - ) - - service, _ = Service.objects.update_or_create( - api_root=settings.AUTORISATIES_API_ROOT, - defaults=dict( - label="Open Zaak Autorisaties API", - client_id=settings.NOTIF_OPENZAAK_CLIENT_ID, - secret=settings.NOTIF_OPENZAAK_SECRET, - user_id=settings.NOTIF_OPENZAAK_CLIENT_ID, - user_representation=f"Open Notificaties {organization}", - ), - ) - - if not service.slug: - slugs = Service.objects.values_list("slug", flat=True) - service.slug = _generate_service_slug(slugs) - service.save(update_fields=("slug",)) - + service = get_service(model.authorizations_api_service_identifier) auth_config.authorizations_api_service = service auth_config.save(update_fields=("component", "authorizations_api_service")) - - def test_configuration(self) -> None: - """ - This check depends on the configuration of permissions in Open Zaak - """ - client = AuthorizationsConfig.get_client() - - if not client: - raise SelfTestFailed("No service configured for the Autorisaties API") - - try: - response: requests.Response = client.get("applicaties") - response.raise_for_status() - except requests.RequestException as exc: - raise SelfTestFailed( - "Could not retrieve list of applications from Autorisaties API." - ) from exc - - -class OpenZaakAuthStep(BaseConfigurationStep): - """ - Configure credentials for Open Zaak to request Open Notificaties - This step takes care only of Open Zaak authentication. Permissions should be - set up in the Autorisaties component of the Open Zaak itself. - - Normal mode doesn't change the secret after its initial creation. - If the secret is changed, run this command with 'overwrite' flag - """ - - verbose_name = "Open Zaak Authentication Configuration" - required_settings = [ - "OPENZAAK_NOTIF_CLIENT_ID", - "OPENZAAK_NOTIF_SECRET", - ] - enable_setting = "OPENZAAK_NOTIF_CONFIG_ENABLE" - - def is_configured(self) -> bool: - return JWTSecret.objects.filter( - identifier=settings.OPENZAAK_NOTIF_CLIENT_ID - ).exists() - - def configure(self): - jwt_secret, created = JWTSecret.objects.get_or_create( - identifier=settings.OPENZAAK_NOTIF_CLIENT_ID, - defaults={"secret": settings.OPENZAAK_NOTIF_SECRET}, - ) - if jwt_secret.secret != settings.OPENZAAK_NOTIF_SECRET: - jwt_secret.secret = settings.OPENZAAK_NOTIF_SECRET - jwt_secret.save(update_fields=["secret"]) - - def test_configuration(self): - """ - This check depends on the configuration of permissions in Open Zaak - """ - endpoint = reverse("kanaal-list", kwargs={"version": "1"}) - full_url = build_absolute_url(endpoint, request=None) - token = generate_jwt( - settings.OPENZAAK_NOTIF_CLIENT_ID, settings.OPENZAAK_NOTIF_SECRET, "", "" - ) - - try: - response = requests.get( - full_url, headers={"Authorization": token, "Accept": "application/json"} - ) - response.raise_for_status() - except requests.RequestException as exc: - raise SelfTestFailed( - f"Could not list kanalen for {settings.NOTIF_OPENZAAK_CLIENT_ID}" - ) from exc diff --git a/src/nrc/config/models.py b/src/nrc/config/models.py new file mode 100644 index 00000000..6aa6e6d5 --- /dev/null +++ b/src/nrc/config/models.py @@ -0,0 +1,20 @@ +from django.contrib.sites.models import Site + +from django_setup_configuration.fields import DjangoModelRef +from django_setup_configuration.models import ConfigurationModel +from pydantic import Field +from vng_api_common.authorizations.models import AuthorizationsConfig + + +class AuthorizationsConfigModel(ConfigurationModel): + authorizations_api_service_identifier: str = DjangoModelRef( + AuthorizationsConfig, "authorizations_api_service" + ) + + +class SiteConfigModel(ConfigurationModel): + organization: str = Field() + """The name of the organization that owns this Open Notificaties instance""" + + class Meta: + django_model_refs = {Site: ("domain",)} diff --git a/src/nrc/config/notification_retry.py b/src/nrc/config/notification_retry.py deleted file mode 100644 index 15b3e3de..00000000 --- a/src/nrc/config/notification_retry.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.conf import settings - -from django_setup_configuration.configuration import BaseConfigurationStep -from notifications_api_common.models import NotificationsConfig - - -class NotificationRetryConfigurationStep(BaseConfigurationStep): - """ - Configure the notifications retry behaviour. - """ - - verbose_name = "Notification retry configuration" - required_settings = [] - optional_settings = [ - "NOTIFICATION_DELIVERY_MAX_RETRIES", - "NOTIFICATION_DELIVERY_RETRY_BACKOFF", - "NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX", - ] - enable_setting = "NOTIFICATION_RETRY_CONFIG_ENABLE" - - def is_configured(self) -> bool: - config = NotificationsConfig.get_solo() - for setting_name in self.optional_settings: - # It is considered configured if one or more fields have non-default values - model_field = getattr(NotificationsConfig, setting_name.lower()).field - if getattr(config, setting_name.lower()) != model_field.default: - return True - return False - - def configure(self): - config = NotificationsConfig.get_solo() - for setting_name in self.optional_settings: - if (setting_value := getattr(settings, setting_name)) is not None: - setattr(config, setting_name.lower(), setting_value) - config.save() - - def test_configuration(self): ... diff --git a/src/nrc/config/site.py b/src/nrc/config/site.py index b85e3fd5..e472982f 100644 --- a/src/nrc/config/site.py +++ b/src/nrc/config/site.py @@ -1,15 +1,11 @@ -from django.conf import settings from django.contrib.sites.models import Site -from django.urls import reverse -import requests from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import SelfTestFailed -from nrc.utils import build_absolute_url +from .models import SiteConfigModel -class SiteConfigurationStep(BaseConfigurationStep): +class SiteConfigurationStep(BaseConfigurationStep[SiteConfigModel]): """ Configure the application site/domain. @@ -17,25 +13,12 @@ class SiteConfigurationStep(BaseConfigurationStep): """ verbose_name = "Site Configuration" - required_settings = ["OPENNOTIFICATIES_DOMAIN", "OPENNOTIFICATIES_ORGANIZATION"] - enable_setting = "SITES_CONFIG_ENABLE" + config_model = SiteConfigModel + namespace = "site_config" + enable_setting = "site_config_enable" - def is_configured(self) -> bool: + def execute(self, model: SiteConfigModel) -> None: site = Site.objects.get_current() - return site.domain == settings.OPENNOTIFICATIES_DOMAIN - - def configure(self): - site = Site.objects.get_current() - site.domain = settings.OPENNOTIFICATIES_DOMAIN - site.name = ( - f"Open Notificaties {settings.OPENNOTIFICATIES_ORGANIZATION}".strip() - ) + site.domain = model.domain + site.name = f"Open Notificaties {model.organization}".strip() site.save() - - def test_configuration(self): - full_url = build_absolute_url(reverse("home")) - try: - response = requests.get(full_url) - response.raise_for_status() - except requests.RequestException as exc: - raise SelfTestFailed(f"Could not access home page at '{full_url}'") from exc From 5bc7a77407b58d2f960dbc5339848622b78f294b Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 6 Dec 2024 11:29:40 +0100 Subject: [PATCH 3/8] :white_check_mark: [#200] Add/fix tests for setup config steps --- .../commands/files/setup_config_full.yaml | 37 +++++ .../commands/test_setup_configuration.py | 111 ++++++-------- .../files/setup_config_auth_config.yaml | 3 + .../config/files/setup_config_sites.yaml | 4 + .../test_authorization_configuration.py | 145 +++++------------- .../test_notification_retry_configuration.py | 31 ---- .../tests/config/test_site_configuration.py | 69 ++++----- 7 files changed, 157 insertions(+), 243 deletions(-) create mode 100644 src/nrc/tests/commands/files/setup_config_full.yaml create mode 100644 src/nrc/tests/config/files/setup_config_auth_config.yaml create mode 100644 src/nrc/tests/config/files/setup_config_sites.yaml delete mode 100644 src/nrc/tests/config/test_notification_retry_configuration.py diff --git a/src/nrc/tests/commands/files/setup_config_full.yaml b/src/nrc/tests/commands/files/setup_config_full.yaml new file mode 100644 index 00000000..e65f118c --- /dev/null +++ b/src/nrc/tests/commands/files/setup_config_full.yaml @@ -0,0 +1,37 @@ +zgw_consumers_config_enable: True +zgw_consumers: + services: + - identifier: autorisaties-api + label: Objecttypen API test + api_root: http://localhost:8001/autorisaties/api/v1/ + api_type: ac + auth_type: zgw + client_id: open-notificaties + secret: oPMsHCEuoP9Qh8vP06D7 + user_id: open-notificaties + user_representation: Open Notificaties Demodam + +autorisaties_api_config_enable: True +autorisaties_api: + # Configure Open Notificaties to make use of Open Zaak's Autorisaties API + authorizations_api_service_identifier: autorisaties-api + +vng_api_common_credentials_config_enable: True +vng_api_common_credentials: + items: + # Credentials for Open Zaak to be able to make requests to Open Notificaties + - identifier: open-zaak + secret: G2LIVfXal1J93puQkV3O + +# notifications_config_enable: True +# notifications_config: +# # No notifications_api_service necessary, because Open Notificaties doesn't send +# # notifications to itself +# notification_delivery_max_retries: 5 +# notification_delivery_retry_backoff: 3 +# notification_delivery_retry_backoff_max: 30 + +# site_config_enable: True +# site_config: +# domain: opennotificaties.local:8000 +# organization: Demodam diff --git a/src/nrc/tests/commands/test_setup_configuration.py b/src/nrc/tests/commands/test_setup_configuration.py index d087bdb9..7c3f229c 100644 --- a/src/nrc/tests/commands/test_setup_configuration.py +++ b/src/nrc/tests/commands/test_setup_configuration.py @@ -1,36 +1,30 @@ import uuid from io import StringIO +from pathlib import Path from django.contrib.sites.models import Site -from django.core.management import CommandError, call_command -from django.test import TestCase, override_settings +from django.core.management import call_command +from django.test import TestCase from django.urls import reverse -import requests import requests_mock from jwt import decode +from notifications_api_common.contrib.setup_configuration.steps import ( + NotificationConfigurationStep, +) +from notifications_api_common.models import NotificationsConfig from rest_framework import status from vng_api_common.authorizations.models import AuthorizationsConfig from vng_api_common.authorizations.utils import generate_jwt +from vng_api_common.contrib.setup_configuration.steps import JWTSecretsConfigurationStep +from zgw_consumers.contrib.setup_configuration.steps import ServiceConfigurationStep -from nrc.config.authorization import AuthorizationStep, OpenZaakAuthStep -from nrc.config.notification_retry import NotificationRetryConfigurationStep +from nrc.config.authorization import AuthorizationStep from nrc.config.site import SiteConfigurationStep +CONFIG_FILE_PATH = Path("src/nrc/tests/commands/files/setup_config_full.yaml").resolve() + -@override_settings( - SITES_CONFIG_ENABLE=True, - OPENNOTIFICATIES_DOMAIN="open-notificaties.example.com", - OPENNOTIFICATIES_ORGANIZATION="ACME", - AUTHORIZATION_CONFIG_ENABLE=True, - AUTORISATIES_API_ROOT="https://oz.example.com/autorisaties/api/v1/", - NOTIF_OPENZAAK_CLIENT_ID="notif-client-id", - NOTIF_OPENZAAK_SECRET="notif-secret", - OPENZAAK_NOTIF_CONFIG_ENABLE=True, - OPENZAAK_NOTIF_CLIENT_ID="oz-client-id", - OPENZAAK_NOTIF_SECRET="oz-secret", - NOTIFICATION_RETRY_CONFIG_ENABLE=True, -) class SetupConfigurationTests(TestCase): maxDiff = None @@ -44,15 +38,15 @@ def test_setup_configuration(self, m): stdout = StringIO() # mocks _uuid = uuid.uuid4() - m.get("http://open-notificaties.example.com/", status_code=200) - m.get("http://open-notificaties.example.com/api/v1/kanaal", json=[]) + m.get("http://opennotificaties.local:8000/", status_code=200) + m.get("http://opennotificaties.local:8000/api/v1/kanaal", json=[]) m.get( - "https://oz.example.com/autorisaties/api/v1/applicaties", + "http://localhost:8001/autorisaties/api/v1/applicaties", json={ "count": 1, "results": [ { - "url": f"https://oz.example.com/autorisaties/api/v1/applicaties/{_uuid}", + "url": f"http://localhost:8001/autorisaties/api/v1/applicaties/{_uuid}", "clientIds": ["oz-client-id"], "label": "OZ for ON", "heeftAlleAutorisaties": True, @@ -62,31 +56,34 @@ def test_setup_configuration(self, m): }, ) - call_command("setup_configuration", stdout=stdout, no_color=True) + call_command( + "setup_configuration", + yaml_file=CONFIG_FILE_PATH, + stdout=stdout, + no_color=True, + ) with self.subTest("Command output"): command_output = stdout.getvalue().splitlines() expected_output = [ - f"Configuration will be set up with following steps: [{SiteConfigurationStep()}, " - f"{AuthorizationStep()}, {OpenZaakAuthStep()}, {NotificationRetryConfigurationStep()}]", - f"Configuring {SiteConfigurationStep()}...", - f"{SiteConfigurationStep()} is successfully configured", - f"Configuring {AuthorizationStep()}...", - f"{AuthorizationStep()} is successfully configured", - f"Configuring {OpenZaakAuthStep()}...", - f"{OpenZaakAuthStep()} is successfully configured", - f"Configuring {NotificationRetryConfigurationStep()}...", - f"{NotificationRetryConfigurationStep()} is successfully configured", + f"Loading config settings from {CONFIG_FILE_PATH}", + "The following steps are configured:", + f"{ServiceConfigurationStep()}", + f"{JWTSecretsConfigurationStep()}", + f"{AuthorizationStep()}", + # f"{NotificationConfigurationStep()}", + # f"{SiteConfigurationStep()}", + "Executing steps...", + f"Successfully executed step: {ServiceConfigurationStep()}", + f"Successfully executed step: {JWTSecretsConfigurationStep()}", + f"Successfully executed step: {AuthorizationStep()}", + # f"Successfully executed step: {NotificationConfigurationStep()}", + # f"Successfully executed step: {SiteConfigurationStep()}", "Instance configuration completed.", ] self.assertEqual(command_output, expected_output) - with self.subTest("Site configured correctly"): - site = Site.objects.get_current() - self.assertEqual(site.domain, "open-notificaties.example.com") - self.assertEqual(site.name, "Open Notificaties ACME") - with self.subTest("Authorization API client configured correctly"): ac_client = AuthorizationsConfig.get_client() self.assertIsNotNone(ac_client) @@ -96,17 +93,17 @@ def test_setup_configuration(self, m): create_call = m.last_request self.assertEqual( create_call.url, - "https://oz.example.com/autorisaties/api/v1/applicaties", + "http://localhost:8001/autorisaties/api/v1/applicaties", ) self.assertIn("Authorization", create_call.headers) header_jwt = create_call.headers["Authorization"].split(" ")[1] decoded_jwt = decode(header_jwt, options={"verify_signature": False}) - self.assertEqual(decoded_jwt["client_id"], "notif-client-id") + self.assertEqual(decoded_jwt["client_id"], "open-notificaties") with self.subTest("Open Zaak can query Notification API"): - token = generate_jwt("oz-client-id", "oz-secret", "", "") + token = generate_jwt("open-zaak", "G2LIVfXal1J93puQkV3O", "", "") response = self.client.get( reverse("kanaal-list", kwargs={"version": 1}), @@ -115,24 +112,14 @@ def test_setup_configuration(self, m): self.assertEqual(response.status_code, status.HTTP_200_OK) - @requests_mock.Mocker() - def test_setup_configuration_selftest_fails(self, m): - m.get("http://open-notificaties.example.com/", exc=requests.ConnectionError) - m.get("http://open-notificaties.example.com/api/v1/kanaal", json=[]) - m.get("https://oz.example.com/autorisaties/api/v1/applicaties", json=[]) - - with self.assertRaisesMessage( - CommandError, - "Could not access home page at 'http://open-notificaties.example.com/'", - ): - call_command("setup_configuration") - - @requests_mock.Mocker() - def test_setup_configuration_without_selftest(self, m): - stdout = StringIO() - - call_command("setup_configuration", no_selftest=True, stdout=stdout) - command_output = stdout.getvalue() - - self.assertEqual(len(m.request_history), 0) - self.assertTrue("Selftest is skipped" in command_output) + # with self.subTest("Notifications configured correctly"): + # config = NotificationsConfig.get_solo() + # self.assertEqual(config.notifications_api_service, None) + # self.assertEqual(config.notification_delivery_max_retries, 5) + # self.assertEqual(config.notification_delivery_retry_backoff, 3) + # self.assertEqual(config.notification_delivery_retry_backoff_max, 30) + + # with self.subTest("Site configured correctly"): + # site = Site.objects.get_current() + # self.assertEqual(site.domain, "opennotificaties.local:8000") + # self.assertEqual(site.name, "Open Notificaties Demodam") diff --git a/src/nrc/tests/config/files/setup_config_auth_config.yaml b/src/nrc/tests/config/files/setup_config_auth_config.yaml new file mode 100644 index 00000000..c8803933 --- /dev/null +++ b/src/nrc/tests/config/files/setup_config_auth_config.yaml @@ -0,0 +1,3 @@ +autorisaties_api_config_enable: True +autorisaties_api: + authorizations_api_service_identifier: autorisaties-api diff --git a/src/nrc/tests/config/files/setup_config_sites.yaml b/src/nrc/tests/config/files/setup_config_sites.yaml new file mode 100644 index 00000000..32ab4db5 --- /dev/null +++ b/src/nrc/tests/config/files/setup_config_sites.yaml @@ -0,0 +1,4 @@ +site_config_enable: True +site_config: + domain: opennotificaties.local:8000 + organization: Demodam diff --git a/src/nrc/tests/config/test_authorization_configuration.py b/src/nrc/tests/config/test_authorization_configuration.py index dfa4fd48..9bcbd146 100644 --- a/src/nrc/tests/config/test_authorization_configuration.py +++ b/src/nrc/tests/config/test_authorization_configuration.py @@ -1,129 +1,58 @@ -from unittest.mock import patch +from django.test import TestCase -from django.test import TestCase, override_settings - -import requests -import requests_mock -from django_setup_configuration.exceptions import SelfTestFailed +from django_setup_configuration.test_utils import execute_single_step from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes -from vng_api_common.models import JWTSecret - -from nrc.config.authorization import AuthorizationStep, OpenZaakAuthStep +from zgw_consumers.test.factories import ServiceFactory +from nrc.config.authorization import AuthorizationStep -@override_settings( - AUTORISATIES_API_ROOT="https://oz.example.com/autorisaties/api/v1/", - NOTIF_OPENZAAK_CLIENT_ID="notif-client-id", - NOTIF_OPENZAAK_SECRET="notif-secret", -) -class AuthorizationConfigurationTests(TestCase): - def test_configure(self): - configuration = AuthorizationStep() +CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_auth_config.yaml" - configuration.configure() - config = AuthorizationsConfig.get_solo() - service = config.authorizations_api_service +class AuthorizationConfigurationTests(TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() - self.assertEqual(config.component, ComponentTypes.nrc) - self.assertEqual( - service.api_root, "https://oz.example.com/autorisaties/api/v1/" + cls.service = ServiceFactory.create( + slug="autorisaties-api", + api_root="http://openzaak.local/autorisaties/api/v1/", ) - self.assertEqual(service.client_id, "notif-client-id") - self.assertEqual(service.secret, "notif-secret") + def test_execute_configuration_step_success(self): + execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) - @requests_mock.Mocker() - def test_selftest_ok(self, m): - configuration = AuthorizationStep() - configuration.configure() - - m.get("https://oz.example.com/autorisaties/api/v1/applicaties", json=[]) + config = AuthorizationsConfig.get_solo() - configuration.test_configuration() + self.assertEqual(config.component, ComponentTypes.nrc) + self.assertEqual(config.authorizations_api_service, self.service) - self.assertEqual( - m.last_request.url, "https://oz.example.com/autorisaties/api/v1/applicaties" - ) + def test_execute_configuration_step_update_existing(self): + config = AuthorizationsConfig.get_solo() + config.component = ComponentTypes.zrc + config.authorizations_api_service = ServiceFactory.create(slug="other-api") + config.save() - @requests_mock.Mocker() - def test_selftest_fail(self, m): - configuration = AuthorizationStep() - configuration.configure() + execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) - m.get("https://oz.example.com/autorisaties/api/v1/applicaties", status_code=403) + config = AuthorizationsConfig.get_solo() + service = config.authorizations_api_service - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() + self.assertEqual(config.component, ComponentTypes.nrc) + self.assertEqual(service, self.service) - self.assertEqual( - m.last_request.url, "https://oz.example.com/autorisaties/api/v1/applicaties" - ) + def test_execute_configuration_step_idempotent(self): + def make_assertions(): + config = AuthorizationsConfig.get_solo() + service = config.authorizations_api_service - def test_is_configured(self): - configuration = AuthorizationStep() - self.assertFalse(configuration.is_configured()) - - configuration.configure() - - self.assertTrue(configuration.is_configured()) - - -@override_settings( - OPENZAAK_NOTIF_CLIENT_ID="oz-client-id", - OPENZAAK_NOTIF_SECRET="oz-secret", -) -class OpenZaakConfigurationTests(TestCase): - def test_configure(self): - configuration = OpenZaakAuthStep() - - configuration.configure() - - jwt_secret = JWTSecret.objects.get(identifier="oz-client-id") - self.assertEqual(jwt_secret.secret, "oz-secret") - - @requests_mock.Mocker() - @patch( - "nrc.config.authorization.build_absolute_url", - return_value="http://testserver/kanaal", - ) - def test_selftest_ok(self, m, *mocks): - configuration = OpenZaakAuthStep() - configuration.configure() - m.get("http://testserver/kanaal", json=[]) - - configuration.test_configuration() - - self.assertEqual(m.last_request.url, "http://testserver/kanaal") - self.assertEqual(m.last_request.method, "GET") - - @requests_mock.Mocker() - @patch( - "nrc.config.authorization.build_absolute_url", - return_value="http://testserver/kanaal", - ) - def test_selftest_fail(self, m, *mocks): - configuration = OpenZaakAuthStep() - configuration.configure() - - mock_kwargs = ( - {"exc": requests.ConnectTimeout}, - {"exc": requests.ConnectionError}, - {"status_code": 404}, - {"status_code": 403}, - {"status_code": 500}, - ) - for mock_config in mock_kwargs: - with self.subTest(mock=mock_config): - m.get("http://testserver/kanaal", **mock_config) + self.assertEqual(config.component, ComponentTypes.nrc) + self.assertEqual(service, self.service) - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() + execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) - def test_is_configured(self): - configuration = OpenZaakAuthStep() - self.assertFalse(configuration.is_configured()) + make_assertions() - configuration.configure() + execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) - self.assertTrue(configuration.is_configured()) + make_assertions() diff --git a/src/nrc/tests/config/test_notification_retry_configuration.py b/src/nrc/tests/config/test_notification_retry_configuration.py deleted file mode 100644 index 6185bc11..00000000 --- a/src/nrc/tests/config/test_notification_retry_configuration.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.test import TestCase, override_settings - -from notifications_api_common.models import NotificationsConfig - -from nrc.config.notification_retry import NotificationRetryConfigurationStep - - -@override_settings( - NOTIFICATION_DELIVERY_MAX_RETRIES=4, - NOTIFICATION_DELIVERY_RETRY_BACKOFF=5, - NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX=6, -) -class NotificationRetryConfigurationTests(TestCase): - def test_configure(self): - configuration = NotificationRetryConfigurationStep() - configuration.configure() - - config = NotificationsConfig.get_solo() - - self.assertEqual(config.notification_delivery_max_retries, 4) - self.assertEqual(config.notification_delivery_retry_backoff, 5) - self.assertEqual(config.notification_delivery_retry_backoff_max, 6) - - def test_is_configured(self): - configuration = NotificationRetryConfigurationStep() - - self.assertFalse(configuration.is_configured()) - - configuration.configure() - - self.assertTrue(configuration.is_configured()) diff --git a/src/nrc/tests/config/test_site_configuration.py b/src/nrc/tests/config/test_site_configuration.py index fb0702e1..f4e32aa5 100644 --- a/src/nrc/tests/config/test_site_configuration.py +++ b/src/nrc/tests/config/test_site_configuration.py @@ -1,66 +1,51 @@ from django.contrib.sites.models import Site -from django.test import TestCase, override_settings +from django.test import TestCase -import requests -import requests_mock -from django_setup_configuration.exceptions import SelfTestFailed +from django_setup_configuration.test_utils import execute_single_step from nrc.config.site import SiteConfigurationStep +CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_sites.yaml" + -@override_settings( - OPENNOTIFICATIES_DOMAIN="localhost:8000", - OPENNOTIFICATIES_ORGANIZATION="ACME", -) class SiteConfigurationTests(TestCase): def setUp(self): super().setUp() self.addCleanup(Site.objects.clear_cache) - def test_set_domain(self): - configuration = SiteConfigurationStep() - configuration.configure() + def test_execute_configuration_step_success(self): + execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) site = Site.objects.get_current() - self.assertEqual(site.domain, "localhost:8000") - self.assertEqual(site.name, "Open Notificaties ACME") - @requests_mock.Mocker() - def test_configuration_check_ok(self, m): - m.get("http://localhost:8000/", status_code=200) - configuration = SiteConfigurationStep() - configuration.configure() + self.assertEqual(site.domain, "opennotificaties.local:8000") + self.assertEqual(site.name, "Open Notificaties Demodam") + + def test_execute_configuration_step_update_existing(self): + site = Site.objects.get_current() + site.domain = "other-domain.local:8000" + site.name = "some other names" + site.save() - configuration.test_configuration() + execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - self.assertEqual(m.last_request.url, "http://localhost:8000/") - self.assertEqual(m.last_request.method, "GET") + site = Site.objects.get_current() - @requests_mock.Mocker() - def test_configuration_check_failures(self, m): - configuration = SiteConfigurationStep() - configuration.configure() + self.assertEqual(site.domain, "opennotificaties.local:8000") + self.assertEqual(site.name, "Open Notificaties Demodam") - mock_kwargs = ( - {"exc": requests.ConnectTimeout}, - {"exc": requests.ConnectionError}, - {"status_code": 404}, - {"status_code": 403}, - {"status_code": 500}, - ) - for mock_config in mock_kwargs: - with self.subTest(mock=mock_config): - m.get("http://localhost:8000/", **mock_config) + def test_execute_configuration_step_idempotent(self): + def make_assertions(): + site = Site.objects.get_current() - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() + self.assertEqual(site.domain, "opennotificaties.local:8000") + self.assertEqual(site.name, "Open Notificaties Demodam") - def test_is_configured(self): - configuration = SiteConfigurationStep() + execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - self.assertFalse(configuration.is_configured()) + make_assertions() - configuration.configure() + execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - self.assertTrue(configuration.is_configured()) + make_assertions() From 28e013ad2a0427ce33b0aa0601f2e458071d9c91 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 6 Dec 2024 11:29:56 +0100 Subject: [PATCH 4/8] :whale: [#200] Update docker setup for setup configuration --- bin/setup_configuration.sh | 2 +- docker-compose.yml | 9 +----- docker/setup_configuration/data.yaml | 48 ++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 docker/setup_configuration/data.yaml diff --git a/bin/setup_configuration.sh b/bin/setup_configuration.sh index ee172d5e..9082f4fe 100755 --- a/bin/setup_configuration.sh +++ b/bin/setup_configuration.sh @@ -6,4 +6,4 @@ #set -e src/manage.py migrate -src/manage.py setup_configuration --no-selftest +src/manage.py setup_configuration --yaml-file setup_configuration/data.yaml diff --git a/docker-compose.yml b/docker-compose.yml index d9303442..d1762c5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,14 +36,6 @@ services: - OPENNOTIFICATIES_SUPERUSER_USERNAME=admin - OPENNOTIFICATIES_SUPERUSER_EMAIL=admin@localhost - DJANGO_SUPERUSER_PASSWORD=admin - # setup_configuration env vars - - OPENNOTIFICATIES_DOMAIN=web:8000 - - OPENNOTIFICATIES_ORGANIZATION=ON - - AUTORISATIES_API_ROOT=https://open-zaak.example.nl/autorisaties/api/v1/ - - NOTIF_OPENZAAK_CLIENT_ID=notif-client-id - - NOTIF_OPENZAAK_SECRET=notif-secret - - OPENZAAK_NOTIF_CLIENT_ID=oz-client-id - - OPENZAAK_NOTIF_SECRET=oz-secret healthcheck: test: ["CMD", "python", "-c", "import requests; exit(requests.head('http://localhost:8000/admin/').status_code not in [200, 302])"] interval: 30s @@ -57,6 +49,7 @@ services: - 8000:8000 volumes: &app-volumes - media:/app/media # Shared media volume to get access to saved OAS files + - ./docker/setup_configuration:/app/setup_configuration depends_on: web-init: condition: service_completed_successfully diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml new file mode 100644 index 00000000..34766a54 --- /dev/null +++ b/docker/setup_configuration/data.yaml @@ -0,0 +1,48 @@ +zgw_consumers_config_enable: True +zgw_consumers: + services: + - identifier: autorisaties-api + label: Autorisaties API + api_root: http://localhost:8000/autorisaties/api/v1/ + api_type: ac + auth_type: zgw + client_id: open-notificaties + secret: oPMsHCEuoP9Qh8vP06D7 + user_id: open-notificaties + user_representation: Open Notificaties Demodam + - identifier: notificaties-api + label: Notificaties API + api_root: http://localhost:8001/api/v1/ + api_type: nrc + auth_type: zgw + client_id: open-notificaties + secret: oPMsHCEuoP9Qh8vP06D7 + user_id: open-notificaties + user_representation: Open Notificaties Demodam + +autorisaties_api_config_enable: True +autorisaties_api: + # Configure Open Notificaties to make use of Open Zaak's Autorisaties API + authorizations_api_service_identifier: autorisaties-api + +vng_api_common_credentials_config_enable: True +vng_api_common_credentials: + items: + # Credentials for Open Zaak to be able to make requests to Open Notificaties + - identifier: open-zaak2 + secret: G2LIVfXal1J93puQkV3O + # Credentials for Open Notificaties, required for autorisaties subscription + - identifier: open-notificaties + secret: oPMsHCEuoP9Qh8vP06D7 + +# notifications_config_enable: True +# notifications_config: +# notifications_api_service_identifier: notificaties-api +# notification_delivery_max_retries: 5 +# notification_delivery_retry_backoff: 3 +# notification_delivery_retry_backoff_max: 30 + +# site_config_enable: True +# site_config: +# domain: localhost:8001 +# organization: Demodam From 9cf61817f78e1d4e8439bcbc67fd0f0165ebbe19 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Fri, 6 Dec 2024 11:32:42 +0100 Subject: [PATCH 5/8] :memo: [#200] Update docs for setup configuration changes --- .../installation/configuration/env_config.rst | 1 + .../configuration/opennotifs_config_cli.rst | 96 ++++++------------- src/nrc/conf/includes/base.py | 6 +- 3 files changed, 33 insertions(+), 70 deletions(-) diff --git a/docs/installation/configuration/env_config.rst b/docs/installation/configuration/env_config.rst index 4eca0904..ebb79cec 100644 --- a/docs/installation/configuration/env_config.rst +++ b/docs/installation/configuration/env_config.rst @@ -110,6 +110,7 @@ Optional * ``LOG_OUTGOING_REQUESTS_MAX_AGE``: The amount of time after which request logs should be deleted from the database. Defaults to: ``7``. * ``SENTRY_DSN``: URL of the sentry project to send error reports to. Default empty, i.e. -> no monitoring set up. Highly recommended to configure this. * ``EXTRA_VERIFY_CERTS``: a comma-separated list of paths to certificates to trust, If you're using self-signed certificates for the services that Open Notificaties communicates with, specify the path to those (root) certificates here, rather than disabling SSL certificate verification. Example: ``EXTRA_VERIFY_CERTS=/etc/ssl/root1.crt,/etc/ssl/root2.crt``. Defaults to: ``(empty string)``. +* ``OPENNOTIFICATIES_DOMAIN``: The domain for this Open Notificaties instance (``[host]:[port]`` or ``[host]``). Defaults to: ``(empty string)``. diff --git a/docs/installation/configuration/opennotifs_config_cli.rst b/docs/installation/configuration/opennotifs_config_cli.rst index 5ef75a99..b04c93f6 100644 --- a/docs/installation/configuration/opennotifs_config_cli.rst +++ b/docs/installation/configuration/opennotifs_config_cli.rst @@ -5,11 +5,8 @@ Open Notificaties configuration (CLI) ===================================== After deploying Open Notificaties, it needs to be configured to be fully functional. The -command line tool ``setup_configuration`` assists with this configuration: - -* It uses environment variables for all configuration choices, therefore you can integrate this with your - infrastructure tooling such as init containers and/or Kubernetes Jobs. -* The command can self-test the configuration to detect problems early on +command line tool ``setup_configuration`` assist with this configuration by loading a +YAML file in which the configuration information is specified. You can get the full command documentation with: @@ -21,96 +18,57 @@ You can get the full command documentation with: running the command and you then run the exact same command again, the manual changes will be reverted. - Preparation =========== The command executes the list of pluggable configuration steps, and each step -requires specific environment variables, that should be prepared. -Here is the description of all available configuration steps and the environment variables, -use by each step. - -Sites configuration -------------------- +requires specific configuration information, that should be prepared. +Here is the description of all available configuration steps and the shape of the data, +used by each step. -Configure the domain where Open Notificaties is hosted -* ``SITES_CONFIG_ENABLE``: enable Site configuration. Defaults to ``False``. -* ``OPENNOTIFICATIES_DOMAIN``: a ``[host]:[port]`` or ``[host]`` value. Required. -* ``OPENNOTIFICATIES_ORGANIZATION``: name of Open Notificaties organization. Required. +Services configuration +---------------------- -Authorization configuration ---------------------------- +TODO: add generated documentation for ``zgw_consumers.ServiceConfigurationStep`` -Open Notificaties uses Open Zaak Authorisaties API to check authorizations -of its consumers, therefore Open Notificaties should be able to request Open Zaak. -Make sure that the correct permissions are configured in Open Zaak Autorisaties API. +Client credentials +------------------ -* ``AUTHORIZATION_CONFIG_ENABLE``: enable Authorization configuration. Defaults - to ``False``. -* ``AUTORISATIES_API_ROOT``: full URL to the Authorisaties API root, for example - ``https://open-zaak.gemeente.local/autorisaties/api/v1/``. Required. -* ``NOTIF_OPENZAAK_CLIENT_ID``: a client id, which Open Notificaties uses to request - Open Zaak, for example, ``open-notificaties``. Required. -* ``NOTIF_OPENZAAK_SECRET``: some random string. Required. +TODO: add generated documentation for ``JWTSecretsConfigurationStep`` -Open Zaak authentication configuration --------------------------------------- -Open Zaak published notifications to the Open Notificaties, therefore it should have access. -Make sure that the correct permissions are configured in Open Zaak Autorisaties API. +Autorisaties API configuration +------------------------------ -* ``OPENZAAK_NOTIF_CONFIG_ENABLE``: enable Open Zaak configuration. Defaults to ``False``. -* ``OPENZAAK_NOTIF_CLIENT_ID``: a client id, which Open Zaak uses to request Open Notificaties, - for example, ``open-zaak``. Required. -* ``OPENZAAK_NOTIF_SECRET``: some random string. Required. +TODO: add generated documentation .. _installation_configuration_cli_retry: -Notification retry configuration --------------------------------- +Notificaties configuration +-------------------------- -Open Notifications has a retry mechanism to guarantee notification delivery, this mechanism -is described in :ref:`delivery_guarantees`. The parameters for this behavior can be configured via the -following environment variables. +TODO: add generated documentation -* ``NOTIFICATION_RETRY_CONFIG_ENABLE``: enable Notification retry configuration. Defaults to ``False``. -* ``NOTIFICATION_DELIVERY_MAX_RETRIES``: the maximum number of retries Celery will do if sending a notification failed. -* ``NOTIFICATION_DELIVERY_RETRY_BACKOFF``: a boolean or a number. If this option is set to - ``True``, autoretries will be delayed following the rules of exponential backoff. If - this option is set to a number, it is used as a delay factor. -* ``NOTIFICATION_DELIVERY_RETRY_BACKOFF_MAX``: an integer, specifying number of seconds. - If ``retry_backoff`` is enabled, this option will set a maximum delay in seconds - between task autoretries. By default, this option is set to 48 seconds. -These settings can also later be changed via the admin interface, under ``Configuration > Notification component configuration``. +Sites configuration +------------------- + +TODO: add generated documentation Execution ========= -With the full command invocation, everything is configured at once and immediately -tested. For all the self-tests to succeed, it's important that the configuration in the -Open Zaak is done before calling this command. - -.. code-block:: bash - - src/manage.py setup_configuration - - -Alternatively, you can skip the self-tests by using the ``--no-selftest`` flag. - -.. code-block:: bash - - src/manage.py setup_configuration --no-self-test - +Open Notificaties configuration +------------------------------- -``setup_configuration`` command checks if the configuration already exists before changing it. -If you want to change some of the values of the existing configuration you can use ``--overwrite`` flag. +With the full command invocation, all defined configuration steps are applied. Each step is idempotent, +so it's safe to run the command multiple times. The steps will overwrite any manual changes made in +the admin if you run the command after making these changes. .. code-block:: bash - src/manage.py setup_configuration --overwrite - + src/manage.py setup_configuration .. note:: Due to a cache-bug in the underlying framework, you need to restart all replicas for part of this change to take effect everywhere. diff --git a/src/nrc/conf/includes/base.py b/src/nrc/conf/includes/base.py index fcccbdf0..5316d161 100644 --- a/src/nrc/conf/includes/base.py +++ b/src/nrc/conf/includes/base.py @@ -161,4 +161,8 @@ # Open Notificaties settings # -OPENNOTIFICATIES_DOMAIN = config("OPENNOTIFICATIES_DOMAIN", "") +OPENNOTIFICATIES_DOMAIN = config( + "OPENNOTIFICATIES_DOMAIN", + "", + help_text="The domain for this Open Notificaties instance (``[host]:[port]`` or ``[host]``)", +) From 1f538bdc3518aa0ef53fb28f9d48e04204639189 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 10 Dec 2024 14:23:27 +0100 Subject: [PATCH 6/8] :truck: [#200] Move setup_config files to setup_configuration directory --- src/nrc/conf/includes/base.py | 4 ++-- src/nrc/{config => setup_configuration}/__init__.py | 0 src/nrc/{config => setup_configuration}/authorization.py | 0 src/nrc/{config => setup_configuration}/models.py | 0 src/nrc/{config => setup_configuration}/site.py | 0 src/nrc/tests/commands/test_setup_configuration.py | 7 +------ src/nrc/tests/{config => setup_configuration}/__init__.py | 0 .../files/setup_config_auth_config.yaml | 0 .../files/setup_config_sites.yaml | 0 .../test_authorization_configuration.py | 2 +- .../test_site_configuration.py | 2 +- 11 files changed, 5 insertions(+), 10 deletions(-) rename src/nrc/{config => setup_configuration}/__init__.py (100%) rename src/nrc/{config => setup_configuration}/authorization.py (100%) rename src/nrc/{config => setup_configuration}/models.py (100%) rename src/nrc/{config => setup_configuration}/site.py (100%) rename src/nrc/tests/{config => setup_configuration}/__init__.py (100%) rename src/nrc/tests/{config => setup_configuration}/files/setup_config_auth_config.yaml (100%) rename src/nrc/tests/{config => setup_configuration}/files/setup_config_sites.yaml (100%) rename src/nrc/tests/{config => setup_configuration}/test_authorization_configuration.py (96%) rename src/nrc/tests/{config => setup_configuration}/test_site_configuration.py (96%) diff --git a/src/nrc/conf/includes/base.py b/src/nrc/conf/includes/base.py index 5316d161..cd5598c5 100644 --- a/src/nrc/conf/includes/base.py +++ b/src/nrc/conf/includes/base.py @@ -20,7 +20,7 @@ # Project applications. "nrc.accounts", "nrc.api", - "nrc.config", + "nrc.setup_configuration", "nrc.datamodel", "nrc.utils", ] @@ -134,7 +134,7 @@ SETUP_CONFIGURATION_STEPS = [ "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", "vng_api_common.contrib.setup_configuration.steps.JWTSecretsConfigurationStep", - "nrc.config.authorization.AuthorizationStep", + "nrc.setup_configuration.authorization.AuthorizationStep", # TODO these need to be implemented later # "notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep", # TODO this should be moved to `django-setup-configuration` diff --git a/src/nrc/config/__init__.py b/src/nrc/setup_configuration/__init__.py similarity index 100% rename from src/nrc/config/__init__.py rename to src/nrc/setup_configuration/__init__.py diff --git a/src/nrc/config/authorization.py b/src/nrc/setup_configuration/authorization.py similarity index 100% rename from src/nrc/config/authorization.py rename to src/nrc/setup_configuration/authorization.py diff --git a/src/nrc/config/models.py b/src/nrc/setup_configuration/models.py similarity index 100% rename from src/nrc/config/models.py rename to src/nrc/setup_configuration/models.py diff --git a/src/nrc/config/site.py b/src/nrc/setup_configuration/site.py similarity index 100% rename from src/nrc/config/site.py rename to src/nrc/setup_configuration/site.py diff --git a/src/nrc/tests/commands/test_setup_configuration.py b/src/nrc/tests/commands/test_setup_configuration.py index 7c3f229c..532d17bd 100644 --- a/src/nrc/tests/commands/test_setup_configuration.py +++ b/src/nrc/tests/commands/test_setup_configuration.py @@ -9,18 +9,13 @@ import requests_mock from jwt import decode -from notifications_api_common.contrib.setup_configuration.steps import ( - NotificationConfigurationStep, -) -from notifications_api_common.models import NotificationsConfig from rest_framework import status from vng_api_common.authorizations.models import AuthorizationsConfig from vng_api_common.authorizations.utils import generate_jwt from vng_api_common.contrib.setup_configuration.steps import JWTSecretsConfigurationStep from zgw_consumers.contrib.setup_configuration.steps import ServiceConfigurationStep -from nrc.config.authorization import AuthorizationStep -from nrc.config.site import SiteConfigurationStep +from nrc.setup_configuration.authorization import AuthorizationStep CONFIG_FILE_PATH = Path("src/nrc/tests/commands/files/setup_config_full.yaml").resolve() diff --git a/src/nrc/tests/config/__init__.py b/src/nrc/tests/setup_configuration/__init__.py similarity index 100% rename from src/nrc/tests/config/__init__.py rename to src/nrc/tests/setup_configuration/__init__.py diff --git a/src/nrc/tests/config/files/setup_config_auth_config.yaml b/src/nrc/tests/setup_configuration/files/setup_config_auth_config.yaml similarity index 100% rename from src/nrc/tests/config/files/setup_config_auth_config.yaml rename to src/nrc/tests/setup_configuration/files/setup_config_auth_config.yaml diff --git a/src/nrc/tests/config/files/setup_config_sites.yaml b/src/nrc/tests/setup_configuration/files/setup_config_sites.yaml similarity index 100% rename from src/nrc/tests/config/files/setup_config_sites.yaml rename to src/nrc/tests/setup_configuration/files/setup_config_sites.yaml diff --git a/src/nrc/tests/config/test_authorization_configuration.py b/src/nrc/tests/setup_configuration/test_authorization_configuration.py similarity index 96% rename from src/nrc/tests/config/test_authorization_configuration.py rename to src/nrc/tests/setup_configuration/test_authorization_configuration.py index 9bcbd146..dd04f2d7 100644 --- a/src/nrc/tests/config/test_authorization_configuration.py +++ b/src/nrc/tests/setup_configuration/test_authorization_configuration.py @@ -4,7 +4,7 @@ from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes from zgw_consumers.test.factories import ServiceFactory -from nrc.config.authorization import AuthorizationStep +from nrc.setup_configuration.authorization import AuthorizationStep CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_auth_config.yaml" diff --git a/src/nrc/tests/config/test_site_configuration.py b/src/nrc/tests/setup_configuration/test_site_configuration.py similarity index 96% rename from src/nrc/tests/config/test_site_configuration.py rename to src/nrc/tests/setup_configuration/test_site_configuration.py index f4e32aa5..29e73489 100644 --- a/src/nrc/tests/config/test_site_configuration.py +++ b/src/nrc/tests/setup_configuration/test_site_configuration.py @@ -3,7 +3,7 @@ from django_setup_configuration.test_utils import execute_single_step -from nrc.config.site import SiteConfigurationStep +from nrc.setup_configuration.site import SiteConfigurationStep CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_sites.yaml" From 83857fc5085a87f7b6a85399876472f5941253a6 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 12 Dec 2024 09:12:31 +0100 Subject: [PATCH 7/8] :ok_hand: [#200] Process PR feedback --- docker/setup_configuration/data.yaml | 12 -- .../installation/configuration/env_config.rst | 1 - src/nrc/conf/includes/base.py | 14 -- src/nrc/setup_configuration/models.py | 11 -- src/nrc/setup_configuration/site.py | 24 ---- .../commands/files/setup_config_full.yaml | 37 ------ .../commands/test_setup_configuration.py | 120 ------------------ .../files/setup_config_sites.yaml | 4 - .../test_authorization_configuration.py | 4 +- .../test_site_configuration.py | 51 -------- src/nrc/utils/__init__.py | 29 ----- 11 files changed, 3 insertions(+), 304 deletions(-) delete mode 100644 src/nrc/setup_configuration/site.py delete mode 100644 src/nrc/tests/commands/files/setup_config_full.yaml delete mode 100644 src/nrc/tests/commands/test_setup_configuration.py delete mode 100644 src/nrc/tests/setup_configuration/files/setup_config_sites.yaml delete mode 100644 src/nrc/tests/setup_configuration/test_site_configuration.py diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 34766a54..2c732bd1 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -34,15 +34,3 @@ vng_api_common_credentials: # Credentials for Open Notificaties, required for autorisaties subscription - identifier: open-notificaties secret: oPMsHCEuoP9Qh8vP06D7 - -# notifications_config_enable: True -# notifications_config: -# notifications_api_service_identifier: notificaties-api -# notification_delivery_max_retries: 5 -# notification_delivery_retry_backoff: 3 -# notification_delivery_retry_backoff_max: 30 - -# site_config_enable: True -# site_config: -# domain: localhost:8001 -# organization: Demodam diff --git a/docs/installation/configuration/env_config.rst b/docs/installation/configuration/env_config.rst index ebb79cec..4eca0904 100644 --- a/docs/installation/configuration/env_config.rst +++ b/docs/installation/configuration/env_config.rst @@ -110,7 +110,6 @@ Optional * ``LOG_OUTGOING_REQUESTS_MAX_AGE``: The amount of time after which request logs should be deleted from the database. Defaults to: ``7``. * ``SENTRY_DSN``: URL of the sentry project to send error reports to. Default empty, i.e. -> no monitoring set up. Highly recommended to configure this. * ``EXTRA_VERIFY_CERTS``: a comma-separated list of paths to certificates to trust, If you're using self-signed certificates for the services that Open Notificaties communicates with, specify the path to those (root) certificates here, rather than disabling SSL certificate verification. Example: ``EXTRA_VERIFY_CERTS=/etc/ssl/root1.crt,/etc/ssl/root2.crt``. Defaults to: ``(empty string)``. -* ``OPENNOTIFICATIES_DOMAIN``: The domain for this Open Notificaties instance (``[host]:[port]`` or ``[host]``). Defaults to: ``(empty string)``. diff --git a/src/nrc/conf/includes/base.py b/src/nrc/conf/includes/base.py index cd5598c5..404db3e2 100644 --- a/src/nrc/conf/includes/base.py +++ b/src/nrc/conf/includes/base.py @@ -135,10 +135,6 @@ "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", "vng_api_common.contrib.setup_configuration.steps.JWTSecretsConfigurationStep", "nrc.setup_configuration.authorization.AuthorizationStep", - # TODO these need to be implemented later - # "notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep", - # TODO this should be moved to `django-setup-configuration` - # "nrc.config.site.SiteConfigurationStep", ] # @@ -156,13 +152,3 @@ "``EXTRA_VERIFY_CERTS=/etc/ssl/root1.crt,/etc/ssl/root2.crt``." ), ) - -# -# Open Notificaties settings -# - -OPENNOTIFICATIES_DOMAIN = config( - "OPENNOTIFICATIES_DOMAIN", - "", - help_text="The domain for this Open Notificaties instance (``[host]:[port]`` or ``[host]``)", -) diff --git a/src/nrc/setup_configuration/models.py b/src/nrc/setup_configuration/models.py index 6aa6e6d5..dec101c6 100644 --- a/src/nrc/setup_configuration/models.py +++ b/src/nrc/setup_configuration/models.py @@ -1,8 +1,5 @@ -from django.contrib.sites.models import Site - from django_setup_configuration.fields import DjangoModelRef from django_setup_configuration.models import ConfigurationModel -from pydantic import Field from vng_api_common.authorizations.models import AuthorizationsConfig @@ -10,11 +7,3 @@ class AuthorizationsConfigModel(ConfigurationModel): authorizations_api_service_identifier: str = DjangoModelRef( AuthorizationsConfig, "authorizations_api_service" ) - - -class SiteConfigModel(ConfigurationModel): - organization: str = Field() - """The name of the organization that owns this Open Notificaties instance""" - - class Meta: - django_model_refs = {Site: ("domain",)} diff --git a/src/nrc/setup_configuration/site.py b/src/nrc/setup_configuration/site.py deleted file mode 100644 index e472982f..00000000 --- a/src/nrc/setup_configuration/site.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.contrib.sites.models import Site - -from django_setup_configuration.configuration import BaseConfigurationStep - -from .models import SiteConfigModel - - -class SiteConfigurationStep(BaseConfigurationStep[SiteConfigModel]): - """ - Configure the application site/domain. - - **NOTE:** Site configuration will be depreciated - """ - - verbose_name = "Site Configuration" - config_model = SiteConfigModel - namespace = "site_config" - enable_setting = "site_config_enable" - - def execute(self, model: SiteConfigModel) -> None: - site = Site.objects.get_current() - site.domain = model.domain - site.name = f"Open Notificaties {model.organization}".strip() - site.save() diff --git a/src/nrc/tests/commands/files/setup_config_full.yaml b/src/nrc/tests/commands/files/setup_config_full.yaml deleted file mode 100644 index e65f118c..00000000 --- a/src/nrc/tests/commands/files/setup_config_full.yaml +++ /dev/null @@ -1,37 +0,0 @@ -zgw_consumers_config_enable: True -zgw_consumers: - services: - - identifier: autorisaties-api - label: Objecttypen API test - api_root: http://localhost:8001/autorisaties/api/v1/ - api_type: ac - auth_type: zgw - client_id: open-notificaties - secret: oPMsHCEuoP9Qh8vP06D7 - user_id: open-notificaties - user_representation: Open Notificaties Demodam - -autorisaties_api_config_enable: True -autorisaties_api: - # Configure Open Notificaties to make use of Open Zaak's Autorisaties API - authorizations_api_service_identifier: autorisaties-api - -vng_api_common_credentials_config_enable: True -vng_api_common_credentials: - items: - # Credentials for Open Zaak to be able to make requests to Open Notificaties - - identifier: open-zaak - secret: G2LIVfXal1J93puQkV3O - -# notifications_config_enable: True -# notifications_config: -# # No notifications_api_service necessary, because Open Notificaties doesn't send -# # notifications to itself -# notification_delivery_max_retries: 5 -# notification_delivery_retry_backoff: 3 -# notification_delivery_retry_backoff_max: 30 - -# site_config_enable: True -# site_config: -# domain: opennotificaties.local:8000 -# organization: Demodam diff --git a/src/nrc/tests/commands/test_setup_configuration.py b/src/nrc/tests/commands/test_setup_configuration.py deleted file mode 100644 index 532d17bd..00000000 --- a/src/nrc/tests/commands/test_setup_configuration.py +++ /dev/null @@ -1,120 +0,0 @@ -import uuid -from io import StringIO -from pathlib import Path - -from django.contrib.sites.models import Site -from django.core.management import call_command -from django.test import TestCase -from django.urls import reverse - -import requests_mock -from jwt import decode -from rest_framework import status -from vng_api_common.authorizations.models import AuthorizationsConfig -from vng_api_common.authorizations.utils import generate_jwt -from vng_api_common.contrib.setup_configuration.steps import JWTSecretsConfigurationStep -from zgw_consumers.contrib.setup_configuration.steps import ServiceConfigurationStep - -from nrc.setup_configuration.authorization import AuthorizationStep - -CONFIG_FILE_PATH = Path("src/nrc/tests/commands/files/setup_config_full.yaml").resolve() - - -class SetupConfigurationTests(TestCase): - maxDiff = None - - def setUp(self): - super().setUp() - - self.addCleanup(Site.objects.clear_cache) - - @requests_mock.Mocker() - def test_setup_configuration(self, m): - stdout = StringIO() - # mocks - _uuid = uuid.uuid4() - m.get("http://opennotificaties.local:8000/", status_code=200) - m.get("http://opennotificaties.local:8000/api/v1/kanaal", json=[]) - m.get( - "http://localhost:8001/autorisaties/api/v1/applicaties", - json={ - "count": 1, - "results": [ - { - "url": f"http://localhost:8001/autorisaties/api/v1/applicaties/{_uuid}", - "clientIds": ["oz-client-id"], - "label": "OZ for ON", - "heeftAlleAutorisaties": True, - "autorisaties": [], - } - ], - }, - ) - - call_command( - "setup_configuration", - yaml_file=CONFIG_FILE_PATH, - stdout=stdout, - no_color=True, - ) - - with self.subTest("Command output"): - command_output = stdout.getvalue().splitlines() - expected_output = [ - f"Loading config settings from {CONFIG_FILE_PATH}", - "The following steps are configured:", - f"{ServiceConfigurationStep()}", - f"{JWTSecretsConfigurationStep()}", - f"{AuthorizationStep()}", - # f"{NotificationConfigurationStep()}", - # f"{SiteConfigurationStep()}", - "Executing steps...", - f"Successfully executed step: {ServiceConfigurationStep()}", - f"Successfully executed step: {JWTSecretsConfigurationStep()}", - f"Successfully executed step: {AuthorizationStep()}", - # f"Successfully executed step: {NotificationConfigurationStep()}", - # f"Successfully executed step: {SiteConfigurationStep()}", - "Instance configuration completed.", - ] - - self.assertEqual(command_output, expected_output) - - with self.subTest("Authorization API client configured correctly"): - ac_client = AuthorizationsConfig.get_client() - self.assertIsNotNone(ac_client) - - ac_client.get("applicaties") - - create_call = m.last_request - self.assertEqual( - create_call.url, - "http://localhost:8001/autorisaties/api/v1/applicaties", - ) - self.assertIn("Authorization", create_call.headers) - - header_jwt = create_call.headers["Authorization"].split(" ")[1] - decoded_jwt = decode(header_jwt, options={"verify_signature": False}) - - self.assertEqual(decoded_jwt["client_id"], "open-notificaties") - - with self.subTest("Open Zaak can query Notification API"): - token = generate_jwt("open-zaak", "G2LIVfXal1J93puQkV3O", "", "") - - response = self.client.get( - reverse("kanaal-list", kwargs={"version": 1}), - HTTP_AUTHORIZATION=token, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # with self.subTest("Notifications configured correctly"): - # config = NotificationsConfig.get_solo() - # self.assertEqual(config.notifications_api_service, None) - # self.assertEqual(config.notification_delivery_max_retries, 5) - # self.assertEqual(config.notification_delivery_retry_backoff, 3) - # self.assertEqual(config.notification_delivery_retry_backoff_max, 30) - - # with self.subTest("Site configured correctly"): - # site = Site.objects.get_current() - # self.assertEqual(site.domain, "opennotificaties.local:8000") - # self.assertEqual(site.name, "Open Notificaties Demodam") diff --git a/src/nrc/tests/setup_configuration/files/setup_config_sites.yaml b/src/nrc/tests/setup_configuration/files/setup_config_sites.yaml deleted file mode 100644 index 32ab4db5..00000000 --- a/src/nrc/tests/setup_configuration/files/setup_config_sites.yaml +++ /dev/null @@ -1,4 +0,0 @@ -site_config_enable: True -site_config: - domain: opennotificaties.local:8000 - organization: Demodam diff --git a/src/nrc/tests/setup_configuration/test_authorization_configuration.py b/src/nrc/tests/setup_configuration/test_authorization_configuration.py index dd04f2d7..95398147 100644 --- a/src/nrc/tests/setup_configuration/test_authorization_configuration.py +++ b/src/nrc/tests/setup_configuration/test_authorization_configuration.py @@ -6,7 +6,9 @@ from nrc.setup_configuration.authorization import AuthorizationStep -CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_auth_config.yaml" +CONFIG_FILE_PATH = ( + "src/nrc/tests/setup_configuration/files/setup_config_auth_config.yaml" +) class AuthorizationConfigurationTests(TestCase): diff --git a/src/nrc/tests/setup_configuration/test_site_configuration.py b/src/nrc/tests/setup_configuration/test_site_configuration.py deleted file mode 100644 index 29e73489..00000000 --- a/src/nrc/tests/setup_configuration/test_site_configuration.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.contrib.sites.models import Site -from django.test import TestCase - -from django_setup_configuration.test_utils import execute_single_step - -from nrc.setup_configuration.site import SiteConfigurationStep - -CONFIG_FILE_PATH = "src/nrc/tests/config/files/setup_config_sites.yaml" - - -class SiteConfigurationTests(TestCase): - def setUp(self): - super().setUp() - - self.addCleanup(Site.objects.clear_cache) - - def test_execute_configuration_step_success(self): - execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - - site = Site.objects.get_current() - - self.assertEqual(site.domain, "opennotificaties.local:8000") - self.assertEqual(site.name, "Open Notificaties Demodam") - - def test_execute_configuration_step_update_existing(self): - site = Site.objects.get_current() - site.domain = "other-domain.local:8000" - site.name = "some other names" - site.save() - - execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - - site = Site.objects.get_current() - - self.assertEqual(site.domain, "opennotificaties.local:8000") - self.assertEqual(site.name, "Open Notificaties Demodam") - - def test_execute_configuration_step_idempotent(self): - def make_assertions(): - site = Site.objects.get_current() - - self.assertEqual(site.domain, "opennotificaties.local:8000") - self.assertEqual(site.name, "Open Notificaties Demodam") - - execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - - make_assertions() - - execute_single_step(SiteConfigurationStep, yaml_source=CONFIG_FILE_PATH) - - make_assertions() diff --git a/src/nrc/utils/__init__.py b/src/nrc/utils/__init__.py index 44cfa3b2..e69de29b 100644 --- a/src/nrc/utils/__init__.py +++ b/src/nrc/utils/__init__.py @@ -1,29 +0,0 @@ -from django.conf import settings -from django.http import HttpRequest - -from furl import furl - - -def get_domain() -> str: - """ - Obtain the domain/netloc of Open Notificaties according to settings or configuration. - """ - from django.contrib.sites.models import Site - - if settings.OPENNOTIFICATIES_DOMAIN: - return settings.OPENNOTIFICATIES_DOMAIN - - return Site.objects.get_current().domain - - -def build_absolute_url(path: str, request: HttpRequest | None = None) -> str: - if request is not None: - return request.build_absolute_uri(path) - - domain = get_domain() - _furl = furl( - scheme="https" if settings.IS_HTTPS else "http", - netloc=domain, - path=path, - ) - return _furl.url From 9072bd3b7ec6753c91e1d910b107fb22bc897b79 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 16 Dec 2024 16:36:34 +0100 Subject: [PATCH 8/8] :ok_hand: [#200] Process PR feedback * add open zaak to docker-compose * use open zaak URL in docker example data * add basic documentation for steps * raise proper exception in step and add test for it --- docker-compose.yml | 42 ++++++++++++++- .../0001-opennotificaties.sql | 0 .../0002-openzaak.sql | 3 ++ .../0003-openzaak-extensions.sh | 7 +++ docker/setup_configuration/data.yaml | 6 +-- .../configuration/opennotifs_config_cli.rst | 54 ++++++++++++++++--- src/nrc/setup_configuration/authorization.py | 17 +++--- .../test_authorization_configuration.py | 18 +++++++ 8 files changed, 127 insertions(+), 20 deletions(-) rename docker-init-db.sql => docker/postgres.entrypoint-initdb.d/0001-opennotificaties.sql (100%) create mode 100644 docker/postgres.entrypoint-initdb.d/0002-openzaak.sql create mode 100755 docker/postgres.entrypoint-initdb.d/0003-openzaak-extensions.sh diff --git a/docker-compose.yml b/docker-compose.yml index d1762c5e..17e8a7a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,11 +3,11 @@ version: '3.4' services: db: - image: postgres:12-alpine + image: postgis/postgis:12-2.5 environment: - POSTGRES_HOST_AUTH_METHOD=trust volumes: - - ./docker-init-db.sql:/docker-entrypoint-initdb.d/init_db.sql + - ./docker/postgres.entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro - db:/var/lib/postgresql/data command: postgres -c max_connections=300 -c log_min_messages=LOG @@ -108,6 +108,44 @@ services: depends_on: - web + openzaak: + image: openzaak/open-zaak:latest + environment: &app-env + - DJANGO_SETTINGS_MODULE=openzaak.conf.docker + - SECRET_KEY=${SECRET_KEY:-7(h1r2hk)8z9+05edulo_3qzymwbo&c24=)qz7+_@3&2sp=u%i} + - DB_NAME=openzaak + - DB_USER=openzaak + - IS_HTTPS=no + - ALLOWED_HOSTS=localhost,127.0.0.1,web,web.local,openzaak + - CORS_ALLOW_ALL_ORIGINS=True + - CSRF_TRUSTED_ORIGINS=http://localhost:9000 + - CACHE_DEFAULT=redis:6379/0 + - CACHE_AXES=redis:6379/0 + - SUBPATH=${SUBPATH:-/} + - IMPORT_DOCUMENTEN_BASE_DIR=${IMPORT_DOCUMENTEN_BASE_DIR:-/app/import-data} + - IMPORT_DOCUMENTEN_BATCH_SIZE=${IMPORT_DOCUMENTEN_BATCH_SIZE:-500} + - OPENZAAK_SUPERUSER_USERNAME=admin + - DJANGO_SUPERUSER_PASSWORD=admin + - OPENZAAK_SUPERUSER_EMAIL=admin@localhost + - CELERY_BROKER_URL=redis://redis:6379/1 + - CELERY_RESULT_BACKEND=redis://redis:6379/1 + - CELERY_LOGLEVEL=DEBUG + - CELERY_WORKER_CONCURRENCY=${CELERY_WORKER_CONCURRENCY:-4} + - ENVIRONMENT=dev + healthcheck: + test: ["CMD", "python", "-c", "import requests; exit(requests.head('http://localhost:8000/admin/').status_code not in [200, 302])"] + interval: 30s + timeout: 5s + retries: 3 + # This should allow for enough time for migrations to run before the max + # retries have passed. This healthcheck in turn allows other containers + # to wait for the database migrations. + start_period: 30s + depends_on: + - db + ports: + - 8001:8000 + volumes: media: db: diff --git a/docker-init-db.sql b/docker/postgres.entrypoint-initdb.d/0001-opennotificaties.sql similarity index 100% rename from docker-init-db.sql rename to docker/postgres.entrypoint-initdb.d/0001-opennotificaties.sql diff --git a/docker/postgres.entrypoint-initdb.d/0002-openzaak.sql b/docker/postgres.entrypoint-initdb.d/0002-openzaak.sql new file mode 100644 index 00000000..66f0c99f --- /dev/null +++ b/docker/postgres.entrypoint-initdb.d/0002-openzaak.sql @@ -0,0 +1,3 @@ +CREATE USER openzaak; +CREATE DATABASE openzaak; +GRANT ALL PRIVILEGES ON DATABASE openzaak TO openzaak; diff --git a/docker/postgres.entrypoint-initdb.d/0003-openzaak-extensions.sh b/docker/postgres.entrypoint-initdb.d/0003-openzaak-extensions.sh new file mode 100755 index 00000000..1adfaeec --- /dev/null +++ b/docker/postgres.entrypoint-initdb.d/0003-openzaak-extensions.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "openzaak" <<-EOSQL + CREATE EXTENSION postgis; + CREATE EXTENSION pg_trgm; +EOSQL diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 2c732bd1..4210b2b0 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -3,16 +3,16 @@ zgw_consumers: services: - identifier: autorisaties-api label: Autorisaties API - api_root: http://localhost:8000/autorisaties/api/v1/ + api_root: http://openzaak:8000/autorisaties/api/v1/ api_type: ac auth_type: zgw client_id: open-notificaties secret: oPMsHCEuoP9Qh8vP06D7 user_id: open-notificaties - user_representation: Open Notificaties Demodam + user_representation: Open Notificaties - identifier: notificaties-api label: Notificaties API - api_root: http://localhost:8001/api/v1/ + api_root: http://web:8000/api/v1/ api_type: nrc auth_type: zgw client_id: open-notificaties diff --git a/docs/installation/configuration/opennotifs_config_cli.rst b/docs/installation/configuration/opennotifs_config_cli.rst index b04c93f6..e28de749 100644 --- a/docs/installation/configuration/opennotifs_config_cli.rst +++ b/docs/installation/configuration/opennotifs_config_cli.rst @@ -8,6 +8,10 @@ After deploying Open Notificaties, it needs to be configured to be fully functio command line tool ``setup_configuration`` assist with this configuration by loading a YAML file in which the configuration information is specified. +.. code-block:: bash + + src/manage.py setup_configuration --yaml-file /path/to/your/yaml + You can get the full command documentation with: .. code-block:: bash @@ -30,7 +34,36 @@ used by each step. Services configuration ---------------------- -TODO: add generated documentation for ``zgw_consumers.ServiceConfigurationStep`` +In order for Open Notificaties to make requests to external services (such as the Autorisaties API), +``Services`` must be configured. To enable this step, set ``zgw_consumers_config_enable`` to ``true`` in your +configuration file and specify a list of ``Services``, for example: + +.. code-block:: yaml + + zgw_consumers_config_enable: true + zgw_consumers: + services: + # all possible configurable fields + - identifier: objecten-test + label: Objecten API test + api_root: http://objecten.local/api/v1/ + api_connection_check_path: objects + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token foo + client_id: client + secret: super-secret + nlx: http://some-outway-adress.local:8080/ + user_id: open-formulieren + user_representation: Open Formulieren + timeout: 5 + # minimum required fields + - identifier: objecttypen-test + label: Objecttypen API test + api_root: http://objecttypen.local/api/v1/ + api_type: orc + auth_type: api_key Client credentials ------------------ @@ -41,7 +74,20 @@ TODO: add generated documentation for ``JWTSecretsConfigurationStep`` Autorisaties API configuration ------------------------------ -TODO: add generated documentation +Open Notificaties uses Autorisaties API to check permissions of the clients that +make requests to Open Notificaties. + +This step configures Open Notificaties to use the specified Autorisaties API (see also :ref:`installation_configuration`). It is +dependent on the `Services configuration`_ step to load a ``Service`` for this Autorisaties API, +which is referred to in this step by ``authorizations_api_service_identifier``. +To enable this step, set ``autorisaties_api_config_enable`` to ``true`` in your +configuration file and specify which ``Service`` to use as the Autorisaties API, for example: + +.. code-block:: yaml + + autorisaties_api_config_enable: True + autorisaties_api: + authorizations_api_service_identifier: autorisaties-api .. _installation_configuration_cli_retry: @@ -66,9 +112,5 @@ With the full command invocation, all defined configuration steps are applied. E so it's safe to run the command multiple times. The steps will overwrite any manual changes made in the admin if you run the command after making these changes. -.. code-block:: bash - - src/manage.py setup_configuration - .. note:: Due to a cache-bug in the underlying framework, you need to restart all replicas for part of this change to take effect everywhere. diff --git a/src/nrc/setup_configuration/authorization.py b/src/nrc/setup_configuration/authorization.py index cea0d93b..43fbb0c1 100644 --- a/src/nrc/setup_configuration/authorization.py +++ b/src/nrc/setup_configuration/authorization.py @@ -1,6 +1,5 @@ -# SPDX-License-Identifier: EUPL-1.2 -# Copyright (C) 2022 Dimpact from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import ConfigurationRunFailed from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes from zgw_consumers.models import Service @@ -15,18 +14,18 @@ def get_service(slug: str) -> Service: try: return Service.objects.get(slug=slug) except Service.DoesNotExist as e: - raise Service.DoesNotExist(f"{str(e)} (identifier = {slug})") + raise ConfigurationRunFailed(f"{str(e)} (identifier = {slug})") class AuthorizationStep(BaseConfigurationStep[AuthorizationsConfigModel]): """ - Open Notificaties uses Autorisaties API to check permissions of the clients. + Open Notificaties uses Autorisaties API to check permissions of the clients that + make requests to Open Notificaties. - 1. Set up authorization to point to the API - 2. Add credentials for Open Notifications to request Open Zaak - - Normal mode doesn't change the credentials after its initial creation. - If the client_id or secret is changed, run this command with 'overwrite' flag + This step configures Open Notificaties to use the specified Autorisaties API. It is + dependent on ``zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep`` + to load a ``Service`` for this Autorisaties API, which is referred to in this step by + ``authorizations_api_service_identifier``. """ verbose_name = "Configuration for Autorisaties API" diff --git a/src/nrc/tests/setup_configuration/test_authorization_configuration.py b/src/nrc/tests/setup_configuration/test_authorization_configuration.py index 95398147..60e7a6be 100644 --- a/src/nrc/tests/setup_configuration/test_authorization_configuration.py +++ b/src/nrc/tests/setup_configuration/test_authorization_configuration.py @@ -1,5 +1,6 @@ from django.test import TestCase +from django_setup_configuration.exceptions import ConfigurationRunFailed from django_setup_configuration.test_utils import execute_single_step from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes from zgw_consumers.test.factories import ServiceFactory @@ -58,3 +59,20 @@ def make_assertions(): execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) make_assertions() + + def test_execute_configuration_service_does_not_exist(self): + self.service.delete() + config = AuthorizationsConfig.get_solo() + config.authorizations_api_service = None + config.save() + + with self.assertRaises(ConfigurationRunFailed) as exc: + execute_single_step(AuthorizationStep, yaml_source=CONFIG_FILE_PATH) + self.assertEqual( + str(exc.exception), + "Service matching query does not exist. (identifier = autorisaties-api)", + ) + + config = AuthorizationsConfig.get_solo() + + self.assertIsNone(config.authorizations_api_service)