diff --git a/.readthedocs.yml b/.readthedocs.yml index c7ddb0e9..099b7e6a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -20,4 +20,5 @@ python: extra_requirements: - notifications - docs + - setup-configuration diff --git a/docs/index.rst b/docs/index.rst index b636fa3a..0959c7eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,6 +33,7 @@ Features :caption: Contents: quickstart + setup_config ref/index diff --git a/docs/setup_config.rst b/docs/setup_config.rst new file mode 100644 index 00000000..02d03241 --- /dev/null +++ b/docs/setup_config.rst @@ -0,0 +1,37 @@ +Setup configuration +=================== + +Loading JWTSecrets from a YAML file +*************************************************** + +This library provides a ``ConfigurationStep`` +(from the library ``django-setup-configuration``, see the +`documentation `_ +for more information on how to run ``setup_configuration``) +to configure the client credentials. + +To add this step to your configuration steps, add ``django_setup_configuration`` to ``INSTALLED_APPS`` and add the following setting: + + .. code:: python + + SETUP_CONFIGURATION_STEPS = [ + ... + "vng_api_common.contrib.setup_configuration.steps.JWTSecretsConfigurationStep" + ... + ] + +The YAML file that is passed to ``setup_configuration`` must set the +``vng_api_common_credentials_config_enable`` flag to ``true`` to enable the step. Any number of ``identifier`` and +``secret`` pairs can be defined under ``vng_api_common_credentials.items`` + +Example file: + + .. code:: yaml + + vng_api_common_credentials_config_enable: True + vng_api_common_credentials: + items: + - identifier: user-id + secret: super-secret + - identifier: user-id2 + secret: super-secret2 diff --git a/setup.cfg b/setup.cfg index e3b03a98..55a146bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -83,6 +83,8 @@ tests = zgw-consumers-oas testutils = zgw-consumers-oas +setup-configuration = + django-setup-configuration>=0.4.0 pep8 = flake8 coverage = pytest-cov docs = diff --git a/testapp/settings.py b/testapp/settings.py index 7433b094..e1abcd85 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -42,6 +42,7 @@ "simple_certmanager", "zgw_consumers", "notifications_api_common", + "django_setup_configuration", "vng_api_common", "vng_api_common.authorizations", "vng_api_common.notifications", diff --git a/tests/files/setup_config_jwtsecrets.yaml b/tests/files/setup_config_jwtsecrets.yaml new file mode 100644 index 00000000..8618d293 --- /dev/null +++ b/tests/files/setup_config_jwtsecrets.yaml @@ -0,0 +1,7 @@ +vng_api_common_credentials_config_enable: True +vng_api_common_credentials: + items: + - identifier: user-id + secret: super-secret + - identifier: user-id2 + secret: super-secret2 diff --git a/tests/test_configuration_steps.py b/tests/test_configuration_steps.py new file mode 100644 index 00000000..7f79d6eb --- /dev/null +++ b/tests/test_configuration_steps.py @@ -0,0 +1,62 @@ +import pytest +from django_setup_configuration.test_utils import execute_single_step + +from vng_api_common.contrib.setup_configuration.steps import JWTSecretsConfigurationStep +from vng_api_common.models import JWTSecret + +CONFIG_FILE_PATH = "tests/files/setup_config_jwtsecrets.yaml" + + +@pytest.mark.django_db +def test_execute_configuration_step_success(): + execute_single_step(JWTSecretsConfigurationStep, yaml_source=CONFIG_FILE_PATH) + + assert JWTSecret.objects.count() == 2 + + credential1, credential2 = JWTSecret.objects.all() + + assert credential1.identifier == "user-id" + assert credential1.secret == "super-secret" + + assert credential2.identifier == "user-id2" + assert credential2.secret == "super-secret2" + + +@pytest.mark.django_db +def test_execute_configuration_step_update_existing(): + JWTSecret.objects.create(identifier="user-id", secret="old") + JWTSecret.objects.create(identifier="user-id2", secret="old2") + + execute_single_step(JWTSecretsConfigurationStep, yaml_source=CONFIG_FILE_PATH) + + assert JWTSecret.objects.count() == 2 + + credential1, credential2 = JWTSecret.objects.all() + + assert credential1.identifier == "user-id" + assert credential1.secret == "super-secret" + + assert credential2.identifier == "user-id2" + assert credential2.secret == "super-secret2" + + +@pytest.mark.django_db +def test_execute_configuration_step_idempotent(): + def make_assertions(): + assert JWTSecret.objects.count() == 2 + + credential1, credential2 = JWTSecret.objects.all() + + assert credential1.identifier == "user-id" + assert credential1.secret == "super-secret" + + assert credential2.identifier == "user-id2" + assert credential2.secret == "super-secret2" + + execute_single_step(JWTSecretsConfigurationStep, yaml_source=CONFIG_FILE_PATH) + + make_assertions() + + execute_single_step(JWTSecretsConfigurationStep, yaml_source=CONFIG_FILE_PATH) + + make_assertions() diff --git a/tox.ini b/tox.ini index a65dae50..9ab47768 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ DJANGO = extras = tests coverage + setup-configuration deps = django42: Django~=4.2.0 setenv = @@ -52,6 +53,7 @@ extras = notifications tests docs + setup-configuration commands= pytest check_sphinx.py -v \ --tb=auto \ diff --git a/vng_api_common/contrib/__init__.py b/vng_api_common/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vng_api_common/contrib/setup_configuration/__init__.py b/vng_api_common/contrib/setup_configuration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vng_api_common/contrib/setup_configuration/models.py b/vng_api_common/contrib/setup_configuration/models.py new file mode 100644 index 00000000..ad96049e --- /dev/null +++ b/vng_api_common/contrib/setup_configuration/models.py @@ -0,0 +1,18 @@ +from django_setup_configuration.models import ConfigurationModel +from pydantic import Field + +from vng_api_common.models import JWTSecret + + +class SingleJWTSecretConfigurationModel(ConfigurationModel): + class Meta: + django_model_refs = { + JWTSecret: [ + "identifier", + "secret", + ] + } + + +class JWTSecretsConfigurationModel(ConfigurationModel): + items: list[SingleJWTSecretConfigurationModel] = Field(default_factory=list) diff --git a/vng_api_common/contrib/setup_configuration/steps.py b/vng_api_common/contrib/setup_configuration/steps.py new file mode 100644 index 00000000..a6bf2a7b --- /dev/null +++ b/vng_api_common/contrib/setup_configuration/steps.py @@ -0,0 +1,23 @@ +from django_setup_configuration.configuration import BaseConfigurationStep + +from vng_api_common.models import JWTSecret + +from .models import JWTSecretsConfigurationModel + + +class JWTSecretsConfigurationStep(BaseConfigurationStep[JWTSecretsConfigurationModel]): + """ + Configure credentials for Applications that need access + """ + + verbose_name = "Configuration to create credentials" + config_model = JWTSecretsConfigurationModel + namespace = "vng_api_common_credentials" + enable_setting = "vng_api_common_credentials_config_enable" + + def execute(self, model: JWTSecretsConfigurationModel): + for config in model.items: + JWTSecret.objects.update_or_create( + identifier=config.identifier, + defaults={"secret": config.secret}, + )