From 539ab505c6ee515175d75a815629d668176b04d2 Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Fri, 15 Nov 2024 14:03:45 +0100 Subject: [PATCH] :construction:[#114] partial implementation of new setup config --- .../setup_configuration/__init__.py | 0 .../setup_configuration/models.py | 56 +++++++++++++++ .../steps.py} | 36 ++++------ pyproject.toml | 2 +- testapp/settings.py | 2 +- tests/setupconfig/conftest.py | 59 ++++----------- tests/setupconfig/files/defaults.yml | 7 ++ tests/setupconfig/files/discovery.yml | 5 ++ .../setupconfig/files/discovery_disabled.yml | 5 ++ tests/setupconfig/files/empty.yml | 2 + tests/setupconfig/files/full_setup.yml | 36 ++++++++++ .../{test_auth.py => test_steps.py} | 72 ++++++++++--------- 12 files changed, 182 insertions(+), 100 deletions(-) create mode 100644 mozilla_django_oidc_db/setup_configuration/__init__.py create mode 100644 mozilla_django_oidc_db/setup_configuration/models.py rename mozilla_django_oidc_db/{setup_config.py => setup_configuration/steps.py} (70%) create mode 100644 tests/setupconfig/files/defaults.yml create mode 100644 tests/setupconfig/files/discovery.yml create mode 100644 tests/setupconfig/files/discovery_disabled.yml create mode 100644 tests/setupconfig/files/empty.yml create mode 100644 tests/setupconfig/files/full_setup.yml rename tests/setupconfig/{test_auth.py => test_steps.py} (75%) diff --git a/mozilla_django_oidc_db/setup_configuration/__init__.py b/mozilla_django_oidc_db/setup_configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mozilla_django_oidc_db/setup_configuration/models.py b/mozilla_django_oidc_db/setup_configuration/models.py new file mode 100644 index 0000000..1c72a05 --- /dev/null +++ b/mozilla_django_oidc_db/setup_configuration/models.py @@ -0,0 +1,56 @@ +from typing import Optional + +from django_setup_configuration.fields import DjangoModelRef +from django_setup_configuration.models import ConfigurationModel +from pydantic import AnyUrl + +from mozilla_django_oidc_db.models import OpenIDConnectConfig + + +class AdminOIDCConfigurationModel(ConfigurationModel): + + # claim_mapping: Optional[str] = None # JSON + + # Arrays are overridden to make the typing simpler (the underlying Django field is an ArrayField, which is non-standard) + username_claim: Optional[list[str]] = DjangoModelRef( + OpenIDConnectConfig, "username_claim" + ) + groups_claim: Optional[list[str]] = DjangoModelRef( + OpenIDConnectConfig, "groups_claim" + ) + superuser_group_names: Optional[list[str]] = DjangoModelRef( + OpenIDConnectConfig, "superuser_group_names" + ) + + # Endpoints + oidc_op_authorization_endpoint: Optional[AnyUrl] = DjangoModelRef( + OpenIDConnectConfig, "oidc_op_authorization_endpoint", required=False + ) + oidc_op_token_endpoint: Optional[AnyUrl] = DjangoModelRef( + OpenIDConnectConfig, "oidc_op_token_endpoint", required=False + ) + oidc_op_user_endpoint: Optional[AnyUrl] = DjangoModelRef( + OpenIDConnectConfig, "oidc_op_user_endpoint", required=False + ) + + class Meta: + django_model_refs = { + OpenIDConnectConfig: [ + "oidc_rp_client_id", + "oidc_rp_client_secret", + "oidc_op_authorization_endpoint", + "oidc_op_token_endpoint", + "oidc_op_user_endpoint", + "oidc_token_use_basic_auth", + "oidc_rp_idp_sign_key", + "oidc_op_logout_endpoint", + "oidc_use_nonce", + "oidc_nonce_size", + "oidc_state_size", + "oidc_keycloak_idp_hint", + "userinfo_claims_source", + "sync_groups", + "sync_groups_glob_pattern", + "make_users_staff", + ] + } diff --git a/mozilla_django_oidc_db/setup_config.py b/mozilla_django_oidc_db/setup_configuration/steps.py similarity index 70% rename from mozilla_django_oidc_db/setup_config.py rename to mozilla_django_oidc_db/setup_configuration/steps.py index baf4e45..0993abd 100644 --- a/mozilla_django_oidc_db/setup_config.py +++ b/mozilla_django_oidc_db/setup_configuration/steps.py @@ -2,37 +2,32 @@ from django.contrib.sessions.middleware import SessionMiddleware from django.test import RequestFactory from django.utils.module_loading import import_string -from django.utils.translation import gettext as _ -from django_setup_configuration.config_settings import ConfigSettings from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed, SelfTestFailed -from .forms import OIDCSetupConfigForm -from .models import OpenIDConnectConfig -from .utils import create_missing_groups +from mozilla_django_oidc_db.forms import OIDCSetupConfigForm +from mozilla_django_oidc_db.models import OpenIDConnectConfig +from mozilla_django_oidc_db.setup_configuration.models import ( + AdminOIDCConfigurationModel, +) +from mozilla_django_oidc_db.utils import create_missing_groups -class AdminOIDCConfigurationStep(BaseConfigurationStep): +class AdminOIDCConfigurationStep(BaseConfigurationStep[AdminOIDCConfigurationModel]): """ Configure admin login via OpenID Connect """ - verbose_name = _("Configuration for admin login via OpenID Connect") + verbose_name = "Configuration for admin login via OpenID Connect" + config_model = AdminOIDCConfigurationModel + namespace = "ADMIN_OIDC" + enable_setting = "ADMIN_OIDC_CONFIG_ENABLE" - config_settings = ConfigSettings( - enable_setting="OIDC_DB_CONFIG_ENABLE", - display_name=_("Admin OIDC Configuration"), - namespace="OIDC_DB", - models=[OpenIDConnectConfig], - update_fields=True, - required_settings=["OIDC_DB_SETUP_CONFIG_ADMIN_AUTH"], - ) - - def is_configured(self) -> bool: + def is_configured(self, model) -> bool: return OpenIDConnectConfig.get_solo().enabled - def configure(self): + def execute(self, model: AdminOIDCConfigurationModel) -> None: config = OpenIDConnectConfig.get_solo() all_settings = { @@ -41,7 +36,7 @@ def configure(self): "enabled": True, "claim_mapping": config.claim_mapping, # JSONFormField widget cannot handle blank values with object schema "sync_groups_glob_pattern": config.sync_groups_glob_pattern, - **settings.OIDC_DB_SETUP_CONFIG_ADMIN_AUTH, + **model.model_dump(), } if groups := all_settings.get("default_groups"): @@ -58,10 +53,9 @@ def configure(self): "Admin OIDC configuration field validation failed", form.errors.as_json(), ) - form.save() - def test_configuration(self): + def validate_result(self, model: AdminOIDCConfigurationModel): request_factory = RequestFactory() request = request_factory.get("/irrelevant") diff --git a/pyproject.toml b/pyproject.toml index c0bb43a..a6e2a6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ Changelog = "https://github.com/maykinmedia/mozilla-django-oidc-db/blob/master/C [project.optional-dependencies] setupconfig = [ - "django-setup-configuration>=0.3.0", + "django-setup-configuration@git+https://github.com/maykinmedia/django-setup-configuration.git@d594a467c43c3323adf118d4a6a249d89dad2535", ] tests = [ "psycopg2", diff --git a/testapp/settings.py b/testapp/settings.py index 72a1fc0..6fbc264 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -89,7 +89,7 @@ OIDC_DB_CONFIG_ENABLE = True SETUP_CONFIGURATION_STEPS = [ - "mozilla_django_oidc_db.setup_config.AdminOIDCConfigurationStep", + "mozilla_django_oidc_db.setup_configuration.steps.AdminOIDCConfigurationStep", ] except ImportError: pass diff --git a/tests/setupconfig/conftest.py b/tests/setupconfig/conftest.py index dd690bd..285ace1 100644 --- a/tests/setupconfig/conftest.py +++ b/tests/setupconfig/conftest.py @@ -1,8 +1,7 @@ import pytest +from django_setup_configuration.test_utils import load_step_config_from_source -from mozilla_django_oidc_db.models import UserInformationClaimsSources - -from ..conftest import KEYCLOAK_BASE_URL +from mozilla_django_oidc_db.setup_configuration.steps import AdminOIDCConfigurationStep """ Key cloak credentials are setup for the keycloak docker-compose.yml. @@ -16,51 +15,21 @@ @pytest.fixture -def setup_config_discovery(settings): - settings.OIDC_DB_SETUP_CONFIG_ADMIN_AUTH = { - "oidc_rp_client_id": "testid", - "oidc_rp_client_secret": "7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I", - "oidc_op_discovery_endpoint": KEYCLOAK_BASE_URL, - } +def setup_config_discovery_model(settings): + return load_step_config_from_source( + AdminOIDCConfigurationStep, "tests/setupconfig/files/discovery.yml" + ) @pytest.fixture -def setup_config_defaults(settings): - settings.OIDC_DB_SETUP_CONFIG_ADMIN_AUTH = { - "oidc_rp_client_id": "client-id", - "oidc_rp_client_secret": "secret", - "oidc_op_authorization_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/auth", - "oidc_op_token_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/token", - "oidc_op_user_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/userinfo", - } +def setup_config_defaults_model(settings): + return load_step_config_from_source( + AdminOIDCConfigurationStep, "tests/setupconfig/files/defaults.yml" + ) @pytest.fixture -def setup_config_full(settings): - - settings.OIDC_DB_SETUP_CONFIG_ADMIN_AUTH = { - "oidc_rp_client_id": "client-id", - "oidc_rp_client_secret": "secret", - "oidc_rp_scopes_list": ["open_id", "email", "profile", "extra_scope"], - "oidc_rp_sign_algo": "RS256", - "oidc_rp_idp_sign_key": "key", - "oidc_op_discovery_endpoint": None, - "oidc_op_jwks_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/certs", - "oidc_op_authorization_endpoint": ( - f"{KEYCLOAK_BASE_URL}protocol/openid-connect/auth" - ), - "oidc_op_token_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/token", - "oidc_op_user_endpoint": f"{KEYCLOAK_BASE_URL}protocol/openid-connect/userinfo", - "username_claim": ["claim_name"], - "groups_claim": ["groups_claim_name"], - "claim_mapping": {"first_name": ["given_name"]}, - "sync_groups": False, - "sync_groups_glob_pattern": "local.groups.*", - "default_groups": ["local.groups.Admins", "local.groups.Read-only"], - "make_users_staff": True, - "superuser_group_names": ["superuser"], - "oidc_use_nonce": False, - "oidc_nonce_size": 48, - "oidc_state_size": 48, - "userinfo_claims_source": UserInformationClaimsSources.id_token, - } +def setup_config_full_model(): + return load_step_config_from_source( + AdminOIDCConfigurationStep, "tests/setupconfig/files/full_setup.yml" + ) diff --git a/tests/setupconfig/files/defaults.yml b/tests/setupconfig/files/defaults.yml new file mode 100644 index 0000000..f31edf6 --- /dev/null +++ b/tests/setupconfig/files/defaults.yml @@ -0,0 +1,7 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: client-id + oidc_rp_client_secret: secret + oidc_op_authorization_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/auth + oidc_op_token_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/token + oidc_op_user_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/userinfo diff --git a/tests/setupconfig/files/discovery.yml b/tests/setupconfig/files/discovery.yml new file mode 100644 index 0000000..c6e2478 --- /dev/null +++ b/tests/setupconfig/files/discovery.yml @@ -0,0 +1,5 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: testid + oidc_rp_client_secret: 7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I + oidc_op_discovery_endpoint: http://localhost:8080/realms/test/ diff --git a/tests/setupconfig/files/discovery_disabled.yml b/tests/setupconfig/files/discovery_disabled.yml new file mode 100644 index 0000000..dc236bd --- /dev/null +++ b/tests/setupconfig/files/discovery_disabled.yml @@ -0,0 +1,5 @@ +ADMIN_OIDC_CONFIG_ENABLE: False +ADMIN_OIDC: + oidc_rp_client_id: testid + oidc_rp_client_secret: 7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I + oidc_op_discovery_endpoint: http://localhost:8080/realms/test/ diff --git a/tests/setupconfig/files/empty.yml b/tests/setupconfig/files/empty.yml new file mode 100644 index 0000000..c950536 --- /dev/null +++ b/tests/setupconfig/files/empty.yml @@ -0,0 +1,2 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: {} diff --git a/tests/setupconfig/files/full_setup.yml b/tests/setupconfig/files/full_setup.yml new file mode 100644 index 0000000..bd88e5c --- /dev/null +++ b/tests/setupconfig/files/full_setup.yml @@ -0,0 +1,36 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: client-id + oidc_rp_client_secret: secret + oidc_rp_scopes_list: + - open_id + - email + - profile + - extra_scope + oidc_rp_sign_algo: RS256 + oidc_rp_idp_sign_key: key + oidc_op_discovery_endpoint: + oidc_op_jwks_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/certs + oidc_op_authorization_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/auth + oidc_op_token_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/token + oidc_op_user_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/userinfo + username_claim: + - claim_name + groups_claim: + - groups_claim_name + claim_mapping: + first_name: + - given_name + sync_groups: false + sync_groups_glob_pattern: local.groups.* + default_groups: + - local.groups.Admins + - local.groups.Read-only + make_users_staff: true + superuser_group_names: + - superuser + oidc_use_nonce: false + oidc_nonce_size: 48 + oidc_state_size: 48 + userinfo_claims_source: id_token + diff --git a/tests/setupconfig/test_auth.py b/tests/setupconfig/test_steps.py similarity index 75% rename from tests/setupconfig/test_auth.py rename to tests/setupconfig/test_steps.py index 9d6d247..0f1bf48 100644 --- a/tests/setupconfig/test_auth.py +++ b/tests/setupconfig/test_steps.py @@ -10,7 +10,7 @@ OpenIDConnectConfig, UserInformationClaimsSources, ) -from mozilla_django_oidc_db.setup_config import AdminOIDCConfigurationStep +from mozilla_django_oidc_db.setup_configuration.steps import AdminOIDCConfigurationStep from ..conftest import KEYCLOAK_BASE_URL @@ -22,10 +22,10 @@ def clear_solo_cache(): @pytest.mark.django_db -def test_configure(setup_config_full): - output = StringIO() - err = StringIO() - call_command("setup_configuration", stdout=output, stderr=err) +def test_configure(setup_config_full_model): + step = AdminOIDCConfigurationStep() + assert not step.is_configured(setup_config_full_model) + step.execute(setup_config_full_model) config = OpenIDConnectConfig.get_solo() @@ -68,36 +68,57 @@ def test_configure(setup_config_full): assert config.oidc_state_size == 48 assert config.userinfo_claims_source == UserInformationClaimsSources.id_token + assert step.is_configured(setup_config_full_model) + @pytest.mark.django_db def test_enable_required_setting(): output = StringIO() err = StringIO() with pytest.raises(CommandError) as command_error: - call_command("setup_configuration", stdout=output, stderr=err) + call_command( + "setup_configuration", + yaml_file=["tests/setupconfig/files/empty.yml"], + stdout=output, + stderr=err, + ) + + assert "Prerequisites for configuration are not fulfilled:" in str( + command_error.value + ) - assert "OIDC_DB_SETUP_CONFIG_ADMIN_AUTH" in str(command_error.value) + assert "ADMIN_OIDC.oidc_rp_client_id" in str(command_error.value) + assert "ADMIN_OIDC.oidc_rp_client_secret" in str(command_error.value) + assert "ADMIN_OIDC.oidc_op_authorization_endpoint" in str(command_error.value) + assert "ADMIN_OIDC.oidc_op_token_endpoint" in str(command_error.value) + assert "ADMIN_OIDC.oidc_op_user_endpoint" in str(command_error.value) config = OpenIDConnectConfig.get_solo() assert not config.enabled @pytest.mark.django_db -def test_enable_setting(settings): - settings.OIDC_DB_CONFIG_ENABLE = False +def test_enable_setting(): output = StringIO() err = StringIO() - call_command("setup_configuration", stdout=output, stderr=err) + with pytest.raises(CommandError) as command_error: + call_command( + "setup_configuration", + yaml_file=["tests/setupconfig/files/discovery_disabled.yml"], + stdout=output, + stderr=err, + ) + + assert "No steps enabled, aborting." in str(command_error.value) config = OpenIDConnectConfig.get_solo() assert not config.enabled @pytest.mark.django_db -def test_configure_use_defaults(setup_config_defaults): - output = StringIO() - err = StringIO() - call_command("setup_configuration", stdout=output, stderr=err) +def test_configure_use_defaults(setup_config_defaults_model): + step = AdminOIDCConfigurationStep() + step.execute(setup_config_defaults_model) config = OpenIDConnectConfig.get_solo() @@ -144,11 +165,9 @@ def test_configure_use_defaults(setup_config_defaults): @pytest.mark.vcr @pytest.mark.django_db -def test_configure_use_discovery_endpoint(setup_config_discovery): - - output = StringIO() - err = StringIO() - call_command("setup_configuration", stdout=output, stderr=err) +def test_configure_use_discovery_endpoint(setup_config_discovery_model): + step = AdminOIDCConfigurationStep() + step.execute(setup_config_discovery_model) config = OpenIDConnectConfig.get_solo() @@ -173,7 +192,7 @@ def test_configure_use_discovery_endpoint(setup_config_discovery): @pytest.mark.django_db -def test_configure_discovery_failure(requests_mock, setup_config_discovery): +def test_configure_discovery_failure(requests_mock, setup_config_discovery_model): mock_kwargs = ( {"exc": requests.ConnectTimeout}, {"exc": requests.ConnectionError}, @@ -188,17 +207,6 @@ def test_configure_discovery_failure(requests_mock, setup_config_discovery): ) with pytest.raises(ConfigurationRunFailed): - AdminOIDCConfigurationStep().configure() + AdminOIDCConfigurationStep().execute(setup_config_discovery_model) assert not OpenIDConnectConfig.get_solo().enabled - - -@pytest.mark.django_db -def test_is_configured(setup_config_full): - config = AdminOIDCConfigurationStep() - - assert not config.is_configured() - - config.configure() - - assert config.is_configured()