From a63657ea205b771156b7ffd435a28697e1ac5d23 Mon Sep 17 00:00:00 2001 From: Conor Holden Date: Tue, 22 Oct 2024 17:22:09 +0200 Subject: [PATCH] :white_check_mark:[#114] add setup configuration tests --- testapp/settings.py | 13 + tests/setupconfig/__init__.py | 0 ...test_configure_use_discovery_endpoint.yaml | 39 +++ tests/setupconfig/conftest.py | 35 +++ tests/setupconfig/files/defaults.yml | 8 + tests/setupconfig/files/discovery.yml | 6 + .../setupconfig/files/discovery_disabled.yml | 6 + tests/setupconfig/files/empty.yml | 2 + tests/setupconfig/files/full_setup.yml | 36 +++ tests/setupconfig/files/partial_endpoints.yml | 8 + tests/setupconfig/test_steps.py | 237 ++++++++++++++++++ 11 files changed, 390 insertions(+) create mode 100644 tests/setupconfig/__init__.py create mode 100644 tests/setupconfig/cassettes/test_steps/test_configure_use_discovery_endpoint.yaml create mode 100644 tests/setupconfig/conftest.py 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 create mode 100644 tests/setupconfig/files/partial_endpoints.yml create mode 100644 tests/setupconfig/test_steps.py diff --git a/testapp/settings.py b/testapp/settings.py index 1ac9c6d..6fbc264 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -80,3 +80,16 @@ LOGIN_REDIRECT_URL = reverse_lazy("admin:index") STATIC_URL = "/static/" + +# Django setup configuration settings +try: + import django_setup_configuration + + INSTALLED_APPS += ["django_setup_configuration"] + + OIDC_DB_CONFIG_ENABLE = True + SETUP_CONFIGURATION_STEPS = [ + "mozilla_django_oidc_db.setup_configuration.steps.AdminOIDCConfigurationStep", + ] +except ImportError: + pass diff --git a/tests/setupconfig/__init__.py b/tests/setupconfig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/setupconfig/cassettes/test_steps/test_configure_use_discovery_endpoint.yaml b/tests/setupconfig/cassettes/test_steps/test_configure_use_discovery_endpoint.yaml new file mode 100644 index 0000000..7323358 --- /dev/null +++ b/tests/setupconfig/cassettes/test_steps/test_configure_use_discovery_endpoint.yaml @@ -0,0 +1,39 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8080/realms/test/.well-known/openid-configuration + response: + body: + string: '{"issuer":"http://localhost:8080/realms/test","authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth","token_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token","introspection_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token/introspect","userinfo_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/userinfo","end_session_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"http://localhost:8080/realms/test/protocol/openid-connect/certs","check_session_iframe":"http://localhost:8080/realms/test/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","implicit","refresh_token","password","client_credentials","urn:openid:params:grant-type:ciba","urn:ietf:params:oauth:grant-type:device_code"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token + token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"id_token_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"http://localhost:8080/realms/test/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","RSA1_5"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","email","roles","phone","profile","address","kvk","web-origins","microprofile-jwt","acr","offline_access","bsn"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","ES384","RS384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","ES384","RS384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token","revocation_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/revoke","introspection_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/token/introspect","device_authorization_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/auth/device","registration_endpoint":"http://localhost:8080/realms/test/clients-registrations/openid-connect","userinfo_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"http://localhost:8080/realms/test/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}' + headers: + Cache-Control: + - no-cache, must-revalidate, no-transform, no-store + Content-Type: + - application/json;charset=UTF-8 + Referrer-Policy: + - no-referrer + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + content-length: + - '5847' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/setupconfig/conftest.py b/tests/setupconfig/conftest.py new file mode 100644 index 0000000..0783f83 --- /dev/null +++ b/tests/setupconfig/conftest.py @@ -0,0 +1,35 @@ +import pytest +from django_setup_configuration.test_utils import build_step_config_from_sources + +from mozilla_django_oidc_db.setup_configuration.steps import AdminOIDCConfigurationStep + +""" +Key cloak credentials are setup for the keycloak docker-compose.yml. + +`oidc_rp_client_id` and `oidc_rp_client_secret` are taken from the keycloak fixture +in /docker/import/test-reaml.json + +See more info in /docker/README.md + +""" + + +@pytest.fixture +def setup_config_discovery_model(settings): + return build_step_config_from_sources( + AdminOIDCConfigurationStep, "tests/setupconfig/files/discovery.yml" + ) + + +@pytest.fixture +def setup_config_defaults_model(settings): + return build_step_config_from_sources( + AdminOIDCConfigurationStep, "tests/setupconfig/files/defaults.yml" + ) + + +@pytest.fixture +def setup_config_full_model(): + return build_step_config_from_sources( + 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..f189ed0 --- /dev/null +++ b/tests/setupconfig/files/defaults.yml @@ -0,0 +1,8 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: client-id + oidc_rp_client_secret: secret + endpoint_config: + 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..3994066 --- /dev/null +++ b/tests/setupconfig/files/discovery.yml @@ -0,0 +1,6 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: testid + oidc_rp_client_secret: 7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I + endpoint_config: + 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..d410413 --- /dev/null +++ b/tests/setupconfig/files/discovery_disabled.yml @@ -0,0 +1,6 @@ +ADMIN_OIDC_CONFIG_ENABLE: False +ADMIN_OIDC: + oidc_rp_client_id: testid + oidc_rp_client_secret: 7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I + endpoint_config: + 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..3b9e08f --- /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_jwks_endpoint: http://localhost:8080/realms/test/protocol/openid-connect/certs + endpoint_config: + 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/files/partial_endpoints.yml b/tests/setupconfig/files/partial_endpoints.yml new file mode 100644 index 0000000..3518bde --- /dev/null +++ b/tests/setupconfig/files/partial_endpoints.yml @@ -0,0 +1,8 @@ +ADMIN_OIDC_CONFIG_ENABLE: True +ADMIN_OIDC: + oidc_rp_client_id: client-id + oidc_rp_client_secret: secret + endpoint_config: + 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/test_steps.py b/tests/setupconfig/test_steps.py new file mode 100644 index 0000000..f5d2eca --- /dev/null +++ b/tests/setupconfig/test_steps.py @@ -0,0 +1,237 @@ +from io import StringIO + +from django.core.management import CommandError, call_command + +import pytest +import requests +from django_setup_configuration.exceptions import ConfigurationRunFailed + +from mozilla_django_oidc_db.models import ( + OpenIDConnectConfig, + UserInformationClaimsSources, +) +from mozilla_django_oidc_db.setup_configuration.steps import AdminOIDCConfigurationStep + +from ..conftest import KEYCLOAK_BASE_URL + + +@pytest.fixture(autouse=True) +def clear_solo_cache(): + yield + OpenIDConnectConfig.clear_cache() + + +@pytest.mark.django_db +def test_configure(setup_config_full_model): + step = AdminOIDCConfigurationStep() + step.execute(setup_config_full_model) + + config = OpenIDConnectConfig.get_solo() + + assert config.enabled + assert config.oidc_rp_client_id == "client-id" + assert config.oidc_rp_client_secret == "secret" + assert config.oidc_rp_scopes_list == ["open_id", "email", "profile", "extra_scope"] + assert config.oidc_rp_sign_algo == "RS256" + assert config.oidc_rp_idp_sign_key == "key" + assert config.oidc_op_discovery_endpoint == "" + assert ( + config.oidc_op_jwks_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/certs" + ) + assert ( + config.oidc_op_authorization_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/auth" + ) + assert ( + config.oidc_op_token_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/token" + ) + assert ( + config.oidc_op_user_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/userinfo" + ) + assert config.username_claim == ["claim_name"] + assert config.groups_claim == ["groups_claim_name"] + assert config.claim_mapping == {"first_name": ["given_name"]} + assert not config.sync_groups + assert config.sync_groups_glob_pattern == "local.groups.*" + assert set(group.name for group in config.default_groups.all()) == { + "local.groups.Admins", + "local.groups.Read-only", + } + assert config.make_users_staff + assert config.superuser_group_names == ["superuser"] + assert not config.oidc_use_nonce + assert config.oidc_nonce_size == 48 + assert config.oidc_state_size == 48 + assert config.userinfo_claims_source == UserInformationClaimsSources.id_token + + +@pytest.mark.django_db +def test_required_settings(): + output = StringIO() + err = StringIO() + with pytest.raises(CommandError) as command_error: + 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 "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.endpoint_config" in str(command_error.value) + + config = OpenIDConnectConfig.get_solo() + assert not config.enabled + + +@pytest.mark.django_db +def test_partial_endpoints_provided(): + """ + Test what if only one endpoint (not discovery) is provided + """ + output = StringIO() + err = StringIO() + with pytest.raises(CommandError) as command_error: + call_command( + "setup_configuration", + yaml_file="tests/setupconfig/files/partial_endpoints.yml", + stdout=output, + stderr=err, + ) + + assert "Prerequisites for configuration are not fulfilled:" in str( + command_error.value + ) + + assert "ADMIN_OIDC.endpoint_config.all.oidc_op_token_endpoint" in str( + command_error.value + ) + assert "ADMIN_OIDC.endpoint_config.all.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(): + output = StringIO() + err = StringIO() + 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_model): + step = AdminOIDCConfigurationStep() + step.execute(setup_config_defaults_model) + + config = OpenIDConnectConfig.get_solo() + + assert config.enabled + assert config.oidc_rp_client_id == "client-id" + assert config.oidc_rp_client_secret == "secret" + assert config.oidc_rp_scopes_list == ["openid", "email", "profile"] + assert config.oidc_rp_sign_algo == "HS256" + assert config.oidc_rp_idp_sign_key == "" + assert config.oidc_op_discovery_endpoint == "" + assert config.oidc_op_jwks_endpoint == "" + + assert ( + config.oidc_op_authorization_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/auth" + ) + assert ( + config.oidc_op_token_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/token" + ) + assert ( + config.oidc_op_user_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/userinfo" + ) + assert config.username_claim == ["sub"] + assert config.groups_claim == ["roles"] + assert config.claim_mapping == { + "last_name": ["family_name"], + "first_name": ["given_name"], + "email": ["email"], + } + assert config.sync_groups + assert config.sync_groups_glob_pattern == "*" + assert config.default_groups.all().count() == 0 + assert not config.make_users_staff + assert config.superuser_group_names == [] + assert config.oidc_use_nonce + assert config.oidc_nonce_size == 32 + assert config.oidc_state_size == 32 + assert ( + config.userinfo_claims_source == UserInformationClaimsSources.userinfo_endpoint + ) + + +@pytest.mark.vcr +@pytest.mark.django_db +def test_configure_use_discovery_endpoint(setup_config_discovery_model): + step = AdminOIDCConfigurationStep() + step.execute(setup_config_discovery_model) + + config = OpenIDConnectConfig.get_solo() + + assert config.enabled + assert config.oidc_op_discovery_endpoint == KEYCLOAK_BASE_URL + assert ( + config.oidc_op_jwks_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/certs" + ) + assert ( + config.oidc_op_authorization_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/auth" + ) + assert ( + config.oidc_op_token_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/token" + ) + assert ( + config.oidc_op_user_endpoint + == f"{KEYCLOAK_BASE_URL}protocol/openid-connect/userinfo" + ) + + +@pytest.mark.django_db +def test_configure_discovery_failure(requests_mock, setup_config_discovery_model): + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + requests_mock.get( + f"{KEYCLOAK_BASE_URL}.well-known/openid-configuration", + **mock_config, + ) + + with pytest.raises(ConfigurationRunFailed): + AdminOIDCConfigurationStep().execute(setup_config_discovery_model) + + assert not OpenIDConnectConfig.get_solo().enabled