diff --git a/docs/installation/configuration/opennotifs_config_cli.rst b/docs/installation/configuration/opennotifs_config_cli.rst index 5ef75a99..d164bac3 100644 --- a/docs/installation/configuration/opennotifs_config_cli.rst +++ b/docs/installation/configuration/opennotifs_config_cli.rst @@ -46,6 +46,7 @@ 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. +* ``OPENNOTIFICATIES_DOMAIN``: a ``[host]:[port]`` or ``[host]`` value. Required. * ``AUTHORIZATION_CONFIG_ENABLE``: enable Authorization configuration. Defaults to ``False``. * ``AUTORISATIES_API_ROOT``: full URL to the Authorisaties API root, for example diff --git a/requirements/base.in b/requirements/base.in index 624d7683..979c05c6 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -6,4 +6,4 @@ self-certifi furl # API libraries -gemma-zds-client +git+https://github.com/maykinmedia/commonground-api-common@feature/update-notifs-client diff --git a/requirements/base.txt b/requirements/base.txt index e7a8d9b4..a7cce01e 100644 --- a/requirements/base.txt +++ b/requirements/base.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 @@ -7,7 +7,10 @@ amqp==5.2.0 # via kombu ape-pie==0.2.0 - # via zgw-consumers + # via + # commonground-api-common + # notifications-api-common + # zgw-consumers asgiref==3.8.1 # via # django @@ -15,8 +18,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 @@ -59,8 +60,10 @@ click-plugins==1.1.1 # via celery click-repl==0.3.0 # via celery -commonground-api-common==1.13.2 - # via open-api-framework +commonground-api-common @ git+https://github.com/maykinmedia/commonground-api-common@feature/update-notifs-client + # via + # -r requirements/base.in + # open-api-framework coreapi==2.3.3 # via commonground-api-common coreschema==0.0.4 @@ -201,11 +204,6 @@ furl==2.1.3 # via # -r requirements/base.in # ape-pie -gemma-zds-client==1.0.1 - # via - # -r requirements/base.in - # commonground-api-common - # notifications-api-common glom==23.5.0 # via mozilla-django-oidc-db humanize==4.10.0 @@ -242,7 +240,7 @@ mozilla-django-oidc==4.0.1 # via mozilla-django-oidc-db mozilla-django-oidc-db==0.19.0 # via open-api-framework -notifications-api-common==0.2.2 +notifications-api-common==0.3.0 # via commonground-api-common open-api-framework==0.8.1 # via -r requirements/base.in @@ -265,7 +263,6 @@ pycparser==2.22 pyjwt==2.9.0 # via # commonground-api-common - # gemma-zds-client # zgw-consumers pyopenssl==24.2.1 # via @@ -289,7 +286,6 @@ pyyaml==6.0.2 # via # drf-spectacular # drf-yasg - # gemma-zds-client # oyaml qrcode==7.4.2 # via django-two-factor-auth @@ -305,7 +301,6 @@ requests==2.32.3 # commonground-api-common # coreapi # django-log-outgoing-requests - # gemma-zds-client # mozilla-django-oidc # open-api-framework # zgw-consumers @@ -330,8 +325,6 @@ tornado==6.4.1 # via flower typing-extensions==4.12.2 # via - # asgiref - # django-solo # mozilla-django-oidc-db # qrcode # zgw-consumers @@ -362,7 +355,8 @@ webencodings==0.5.1 # via bleach wrapt==1.16.0 # via elastic-apm -zgw-consumers==0.34.0 +zgw-consumers==0.35.1 # via + # commonground-api-common # notifications-api-common # open-api-framework diff --git a/requirements/ci.txt b/requirements/ci.txt index 27cb8488..8aa6ac24 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 @@ -13,6 +13,8 @@ amqp==5.2.0 ape-pie==0.2.0 # via # -r requirements/base.txt + # commonground-api-common + # notifications-api-common # zgw-consumers asgiref==3.8.1 # via @@ -24,10 +26,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 @@ -100,7 +98,7 @@ click-repl==0.3.0 # celery codecov==2.1.13 # via -r requirements/ci.in -commonground-api-common==1.13.2 +commonground-api-common @ git+https://github.com/maykinmedia/commonground-api-common@feature/update-notifs-client # via # -r requirements/base.txt # open-api-framework @@ -315,8 +313,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 @@ -337,11 +333,6 @@ furl==2.1.3 # via # -r requirements/base.txt # ape-pie -gemma-zds-client==1.0.1 - # via - # -r requirements/base.txt - # commonground-api-common - # notifications-api-common glom==23.5.0 # via # -r requirements/base.txt @@ -425,7 +416,7 @@ multidict==6.0.5 # via yarl mypy-extensions==0.4.3 # via black -notifications-api-common==0.2.2 +notifications-api-common==0.3.0 # via # -r requirements/base.txt # commonground-api-common @@ -484,7 +475,6 @@ pyjwt==2.9.0 # via # -r requirements/base.txt # commonground-api-common - # gemma-zds-client # zgw-consumers pyopenssl==24.2.1 # via @@ -524,7 +514,6 @@ pyyaml==6.0.2 # -r requirements/base.txt # drf-spectacular # drf-yasg - # gemma-zds-client # oyaml # vcrpy qrcode==7.4.2 @@ -550,7 +539,6 @@ requests==2.32.3 # commonground-api-common # coreapi # django-log-outgoing-requests - # gemma-zds-client # mozilla-django-oidc # open-api-framework # requests-mock @@ -612,11 +600,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 @@ -624,9 +607,6 @@ tornado==6.4.1 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref - # black - # django-solo # mozilla-django-oidc-db # qrcode # zgw-consumers @@ -683,8 +663,9 @@ wrapt==1.16.0 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.34.0 +zgw-consumers==0.35.1 # via # -r requirements/base.txt + # commonground-api-common # notifications-api-common # open-api-framework diff --git a/requirements/dev.txt b/requirements/dev.txt index 91971b6f..b35547bc 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 @@ -13,6 +13,8 @@ amqp==5.2.0 ape-pie==0.2.0 # via # -r requirements/base.txt + # commonground-api-common + # notifications-api-common # zgw-consumers asgiref==3.8.1 # via @@ -24,10 +26,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 @@ -103,7 +101,7 @@ click-repl==0.3.0 # via # -r requirements/base.txt # celery -commonground-api-common==1.13.2 +commonground-api-common @ git+https://github.com/maykinmedia/commonground-api-common@feature/update-notifs-client # via # -r requirements/base.txt # open-api-framework @@ -342,11 +340,6 @@ furl==2.1.3 # via # -r requirements/base.txt # ape-pie -gemma-zds-client==1.0.1 - # via - # -r requirements/base.txt - # commonground-api-common - # notifications-api-common gitdb==4.0.9 # via gitpython gitpython==3.1.41 @@ -432,7 +425,7 @@ multidict==6.0.5 # via yarl mypy-extensions==0.4.3 # via black -notifications-api-common==0.2.2 +notifications-api-common==0.3.0 # via # -r requirements/base.txt # commonground-api-common @@ -491,7 +484,6 @@ pyjwt==2.9.0 # via # -r requirements/base.txt # commonground-api-common - # gemma-zds-client # zgw-consumers pyopenssl==24.2.1 # via @@ -531,7 +523,6 @@ pyyaml==6.0.2 # -r requirements/base.txt # drf-spectacular # drf-yasg - # gemma-zds-client # oyaml # vcrpy qrcode==7.4.2 @@ -556,7 +547,6 @@ requests==2.32.3 # commonground-api-common # coreapi # django-log-outgoing-requests - # gemma-zds-client # mozilla-django-oidc # open-api-framework # requests-mock @@ -618,12 +608,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 @@ -631,9 +615,6 @@ tornado==6.4.1 typing-extensions==4.12.2 # via # -r requirements/base.txt - # asgiref - # black - # django-solo # mozilla-django-oidc-db # qrcode # zgw-consumers @@ -692,9 +673,10 @@ wrapt==1.16.0 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.34.0 +zgw-consumers==0.35.1 # via # -r requirements/base.txt + # commonground-api-common # notifications-api-common # open-api-framework diff --git a/src/nrc/api/admin.py b/src/nrc/api/admin.py index c1214fba..78aef966 100644 --- a/src/nrc/api/admin.py +++ b/src/nrc/api/admin.py @@ -3,7 +3,8 @@ from django.utils.translation import gettext_lazy as _ from vng_api_common.models import JWTSecret -from zds_client import ClientAuth + +from nrc.utils.auth import generate_jwt admin.site.unregister(JWTSecret) @@ -18,8 +19,9 @@ class JWTSecretAdmin(admin.ModelAdmin): @admin.display(description="jwt") def get_jwt(self, obj): if obj.identifier and obj.secret: - auth = ClientAuth(obj.identifier, obj.secret) - jwt = auth.credentials()["Authorization"] + jwt = generate_jwt( + obj.identifier, obj.secret, obj.identifier, obj.identifier + ) return format_html( '{val}

{hint}

', val=jwt, diff --git a/src/nrc/config/authorization.py b/src/nrc/config/authorization.py index e15f6c58..f5c2ae88 100644 --- a/src/nrc/config/authorization.py +++ b/src/nrc/config/authorization.py @@ -6,11 +6,15 @@ import requests from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import SelfTestFailed +from furl import furl +from notifications_api_common.models import NotificationsConfig from vng_api_common.authorizations.models import AuthorizationsConfig, ComponentTypes from vng_api_common.models import APICredential, JWTSecret -from zds_client import ClientAuth +from zgw_consumers.constants import APITypes, AuthTypes +from zgw_consumers.models import Service from nrc.utils import build_absolute_url +from nrc.utils.auth import generate_jwt class AuthorizationStep(BaseConfigurationStep): @@ -19,6 +23,8 @@ class AuthorizationStep(BaseConfigurationStep): 1. Set up authorization to point to the API 2. Add credentials for Open Notifications to request Open Zaak + 3. Configure Open Notificaties such that it can access itself (required because + Open Notificaties must be subscribed to changes in the `autorisaties` channel) 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 @@ -26,6 +32,7 @@ class AuthorizationStep(BaseConfigurationStep): verbose_name = "Authorization Configuration" required_settings = [ + "OPENNOTIFICATIES_DOMAIN", "AUTORISATIES_API_ROOT", "NOTIF_OPENZAAK_CLIENT_ID", "NOTIF_OPENZAAK_SECRET", @@ -65,6 +72,35 @@ def configure(self) -> None: }, ) + # TODO remove hardcoded version? + # Step 3 (step 8/9 in Open Zaak configuration documentation) + api_version = settings.API_VERSION.split(".")[0] + notifs_api_root = ( + furl(settings.OPENNOTIFICATIES_DOMAIN) + / reverse("api-root", kwargs={"version": api_version}) + ).url + notifs_oas_url = ( + furl(settings.OPENNOTIFICATIES_DOMAIN) + / reverse("schema", kwargs={"version": api_version}) + ).url + scheme = "http" if settings.DEBUG else "https" + notification_service, _ = Service.objects.update_or_create( + api_root=f"{scheme}://{notifs_api_root}", + oas=f"{scheme}://{notifs_oas_url}", + defaults={ + "label": "Open Notificaties", + "api_type": APITypes.nrc, + "client_id": settings.NOTIF_OPENZAAK_CLIENT_ID, + "secret": settings.NOTIF_OPENZAAK_SECRET, + "auth_type": AuthTypes.zgw, + "user_id": settings.NOTIF_OPENZAAK_CLIENT_ID, + "user_representation": f"Open Notificaties {organization}", + }, + ) + config = NotificationsConfig.get_solo() + config.notifications_api_service = notification_service + config.save() + def test_configuration(self) -> None: """ This check depends on the configuration of permissions in Open Zaak @@ -115,14 +151,16 @@ def test_configuration(self): """ endpoint = reverse("kanaal-list", kwargs={"version": "1"}) full_url = build_absolute_url(endpoint, request=None) - auth = ClientAuth( - client_id=settings.OPENZAAK_NOTIF_CLIENT_ID, - secret=settings.OPENZAAK_NOTIF_SECRET, + token = generate_jwt( + settings.OPENZAAK_NOTIF_CLIENT_ID, + settings.OPENZAAK_NOTIF_SECRET, + settings.OPENZAAK_NOTIF_CLIENT_ID, + settings.OPENZAAK_NOTIF_CLIENT_ID, ) try: response = requests.get( - full_url, headers={**auth.credentials(), "Accept": "application/json"} + full_url, headers={"Authorization": token, "Accept": "application/json"} ) response.raise_for_status() except requests.RequestException as exc: diff --git a/src/nrc/tests/commands/test_setup_configuration.py b/src/nrc/tests/commands/test_setup_configuration.py index 4c43b9e5..94df67d6 100644 --- a/src/nrc/tests/commands/test_setup_configuration.py +++ b/src/nrc/tests/commands/test_setup_configuration.py @@ -9,15 +9,17 @@ import requests import requests_mock from jwt import decode +from notifications_api_common.models import NotificationsConfig from rest_framework import status from vng_api_common.authorizations.models import AuthorizationsConfig -from zds_client.auth import ClientAuth -from zgw_consumers.constants import APITypes +from zgw_consumers.constants import APITypes, AuthTypes +from zgw_consumers.models import Service from zgw_consumers.test import mock_service_oas_get from nrc.config.authorization import AuthorizationStep, OpenZaakAuthStep from nrc.config.notification_retry import NotificationRetryConfigurationStep from nrc.config.site import SiteConfigurationStep +from nrc.utils.auth import generate_jwt @override_settings( @@ -109,15 +111,46 @@ def test_setup_configuration(self, m): self.assertEqual(decoded_jwt["client_id"], "notif-client-id") with self.subTest("Open Zaak can query Notification API"): - auth = ClientAuth("oz-client-id", "oz-secret") + token = generate_jwt( + "oz-client-id", "oz-secret", "oz-client-id", "oz-client-id" + ) response = self.client.get( reverse("kanaal-list", kwargs={"version": 1}), - HTTP_AUTHORIZATION=auth.credentials()["Authorization"], + HTTP_AUTHORIZATION=token, ) self.assertEqual(response.status_code, status.HTTP_200_OK) + with self.subTest("Open Notificaties can access itself"): + notifications_service = Service.objects.get() + + self.assertEqual( + notifications_service.api_root, + "https://open-notificaties.example.com/api/v1/", + ) + self.assertEqual( + notifications_service.oas, + "https://open-notificaties.example.com/api/v1/schema/openapi.yaml", + ) + self.assertEqual(notifications_service.label, "Open Notificaties") + self.assertEqual(notifications_service.api_type, APITypes.nrc) + self.assertEqual(notifications_service.client_id, "notif-client-id") + self.assertEqual(notifications_service.secret, "notif-secret") + self.assertEqual(notifications_service.auth_type, AuthTypes.zgw) + self.assertEqual(notifications_service.user_id, "notif-client-id") + self.assertEqual( + notifications_service.user_representation, "Open Notificaties ACME" + ) + + config = NotificationsConfig.get_solo() + + self.assertEqual(config.notifications_api_service, notifications_service) + # resp = self.client.get("/view-config/") + + # import pdb; pdb.set_trace() + # TODO add test for service creation and check view config? + @requests_mock.Mocker() def test_setup_configuration_selftest_fails(self, m): m.get("http://open-notificaties.example.com/", exc=requests.ConnectionError) diff --git a/src/nrc/utils/auth.py b/src/nrc/utils/auth.py new file mode 100644 index 00000000..e8c58ac2 --- /dev/null +++ b/src/nrc/utils/auth.py @@ -0,0 +1,18 @@ +def generate_jwt(client_id, secret, user_id, user_representation): + # TODO fix this workaround + from zgw_consumers.client import ZGWAuth + + class FakeService: + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + auth = ZGWAuth( + service=FakeService( # type: ignore + client_id=client_id, + secret=secret, + user_id=user_id, + user_representation=user_representation, + ) + ) + return f"Bearer {auth._token}"