From 7ce480bd5ca6e21c15b864d47d1dd2dfff013096 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 13 May 2025 09:16:34 +0200 Subject: [PATCH] Solved oauth2 proxy configuration bugs --- roles/docker-central-database/tasks/main.yml | 2 +- roles/docker-oauth2-proxy/tasks/main.yml | 2 +- .../templates/oauth2-proxy-keycloak.cfg.j2 | 18 ++-- roles/docker-oauth2-proxy/vars/main.yml | 1 + roles/nginx-domain-setup/tasks/main.yml | 9 +- roles/nginx-domain-setup/vars/main.yml | 3 +- tests/unit/test_configuration_filters.py | 92 +++++++++++++++++++ 7 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 roles/docker-oauth2-proxy/vars/main.yml create mode 100644 tests/unit/test_configuration_filters.py diff --git a/roles/docker-central-database/tasks/main.yml b/roles/docker-central-database/tasks/main.yml index 7a14279d..675a0f69 100644 --- a/roles/docker-central-database/tasks/main.yml +++ b/roles/docker-central-database/tasks/main.yml @@ -1,4 +1,4 @@ -- name: "set _tmp_database_application_id (Needed due to lazzy loading issue)" +- name: "set database_application_id (Needed due to lazzy loading issue)" set_fact: database_application_id: "{{ application_id }}" diff --git a/roles/docker-oauth2-proxy/tasks/main.yml b/roles/docker-oauth2-proxy/tasks/main.yml index 8fe35d02..80052edf 100644 --- a/roles/docker-oauth2-proxy/tasks/main.yml +++ b/roles/docker-oauth2-proxy/tasks/main.yml @@ -1,6 +1,6 @@ - name: "Transfering oauth2-proxy-keycloak.cfg.j2 to {{docker_compose.directories.volumes}}" template: src: oauth2-proxy-keycloak.cfg.j2 - dest: "{{docker_compose.directories.volumes}}{{applications.oauth2_proxy.configuration_file}}" + dest: "{{docker_compose.directories.volumes}}{{applications[application_id].configuration_file}}" notify: - docker compose project setup \ No newline at end of file diff --git a/roles/docker-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2 b/roles/docker-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2 index 431490a3..c4eb6d18 100644 --- a/roles/docker-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2 +++ b/roles/docker-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2 @@ -1,20 +1,20 @@ http_address = "0.0.0.0:4180" -cookie_secret = "{{ applications[application_id].credentials.oauth2_proxy_cookie_secret }}" -email_domains = "{{primary_domain}}" +cookie_secret = "{{ applications[oauth2_proxy_application_id].credentials.oauth2_proxy_cookie_secret }}" +email_domains = "{{ primary_domain }}" cookie_secure = "true" # True is necessary to force the cookie set via https -upstreams = "http://{{applications[application_id].oauth2_proxy.application}}:{{applications[application_id].oauth2_proxy.port}}" -cookie_domains = ["{{domain}}", "{{domains.keycloak}}"] # Required so cookie can be read on all subdomains. -whitelist_domains = [".{{primary_domain}}"] # Required to allow redirection back to original requested target. +upstreams = "http://{{ applications[oauth2_proxy_application_id].oauth2_proxy.application }}:{{ applications[oauth2_proxy_application_id].oauth2_proxy.port }}" +cookie_domains = ["{{ domain }}", "{{ domains.keycloak }}"] # Required so cookie can be read on all subdomains. +whitelist_domains = [".{{ primary_domain }}"] # Required to allow redirection back to original requested target. # keycloak provider -client_secret = "{{oidc.client.secret}}" -client_id = "{{oidc.client.id}}" +client_secret = "{{ oidc.client.secret }}" +client_id = "{{ oidc.client.id }}" redirect_url = "{{ web_protocol }}://{{domain}}/oauth2/callback" -oidc_issuer_url = "{{oidc.client.issuer_url}}" +oidc_issuer_url = "{{ oidc.client.issuer_url }}" provider = "oidc" provider_display_name = "Keycloak" # role restrictions #cookie_roles = "realm_access.roles" -#allowed_groups = "{{applications.oauth2_proxy.allowed_roles}}" # This is not correct here. needs to be placed in applications @todo move there when implementing +#allowed_groups = "{{ applications[oauth2_proxy_application_id].allowed_roles }}" # This is not correct here. needs to be placed in applications @todo move there when implementing # @see https://chatgpt.com/share/67f42607-bf68-800f-b587-bd56fe9067b5 \ No newline at end of file diff --git a/roles/docker-oauth2-proxy/vars/main.yml b/roles/docker-oauth2-proxy/vars/main.yml new file mode 100644 index 00000000..6c226977 --- /dev/null +++ b/roles/docker-oauth2-proxy/vars/main.yml @@ -0,0 +1 @@ +application_id: oauth2-proxy \ No newline at end of file diff --git a/roles/nginx-domain-setup/tasks/main.yml b/roles/nginx-domain-setup/tasks/main.yml index b8b3b88b..29a599a0 100644 --- a/roles/nginx-domain-setup/tasks/main.yml +++ b/roles/nginx-domain-setup/tasks/main.yml @@ -7,8 +7,13 @@ src: "{{ vhost_template_src }}" dest: "{{ configuration_destination }}" notify: restart nginx - + +- name: "set oauth2_proxy_application_id (Needed due to lazzy loading issue)" + set_fact: + oauth2_proxy_application_id: "{{ application_id }}" + when: "{{applications[application_id].get('features', {}).get('oauth2', False)}}" + - name: "include the docker-oauth2-proxy role {{domain}}" include_role: name: docker-oauth2-proxy - when: final_oauth2_enabled | bool \ No newline at end of file + when: "{{applications[application_id].get('features', {}).get('oauth2', False)}}" \ No newline at end of file diff --git a/roles/nginx-domain-setup/vars/main.yml b/roles/nginx-domain-setup/vars/main.yml index 53006072..6a6c2417 100644 --- a/roles/nginx-domain-setup/vars/main.yml +++ b/roles/nginx-domain-setup/vars/main.yml @@ -1,2 +1 @@ -configuration_destination: "{{nginx.directories.http.servers}}{{domain}}.conf" -final_oauth2_enabled: "{{applications[application_id].get('features', {}).get('oauth2', False)}}" \ No newline at end of file +configuration_destination: "{{nginx.directories.http.servers}}{{domain}}.conf" \ No newline at end of file diff --git a/tests/unit/test_configuration_filters.py b/tests/unit/test_configuration_filters.py new file mode 100644 index 00000000..8c5fb18c --- /dev/null +++ b/tests/unit/test_configuration_filters.py @@ -0,0 +1,92 @@ +# tests/unit/test_configuration_filters.py + +import unittest +from filter_plugins.configuration_filters import ( + is_feature_enabled, + get_csp_whitelist, + get_csp_flags, +) + + +class TestConfigurationFilters(unittest.TestCase): + def setUp(self): + # Sample applications data for testing + self.applications = { + 'app1': { + 'features': { + 'oauth2': True, + }, + 'csp': { + 'whitelist': { + # directive with a list + 'script-src': ['https://example.com'], + # directive with a single string + 'connect-src': 'https://api.example.com', + }, + 'flags': { + # both flags for script-src + 'script-src': { + 'unsafe_eval': True, + 'unsafe_inline': False, + }, + # only unsafe_inline for style-src + 'style-src': { + 'unsafe_inline': True, + }, + }, + }, + }, + 'app2': { + # no features or csp defined + }, + } + + # Tests for is_feature_enabled + def test_is_feature_enabled_true(self): + self.assertTrue(is_feature_enabled(self.applications, 'oauth2', 'app1')) + + def test_is_feature_enabled_false_missing_feature(self): + self.assertFalse(is_feature_enabled(self.applications, 'nonexistent', 'app1')) + + def test_is_feature_enabled_false_missing_app(self): + self.assertFalse(is_feature_enabled(self.applications, 'oauth2', 'unknown_app')) + + # Tests for get_csp_whitelist + def test_get_csp_whitelist_returns_list_as_is(self): + result = get_csp_whitelist(self.applications, 'app1', 'script-src') + self.assertEqual(result, ['https://example.com']) + + def test_get_csp_whitelist_wraps_string_in_list(self): + result = get_csp_whitelist(self.applications, 'app1', 'connect-src') + self.assertEqual(result, ['https://api.example.com']) + + def test_get_csp_whitelist_empty_when_not_defined(self): + result = get_csp_whitelist(self.applications, 'app1', 'frame-src') + self.assertEqual(result, []) + + def test_get_csp_whitelist_empty_when_app_missing(self): + result = get_csp_whitelist(self.applications, 'nonexistent_app', 'script-src') + self.assertEqual(result, []) + + # Tests for get_csp_flags + def test_get_csp_flags_includes_unsafe_eval(self): + result = get_csp_flags(self.applications, 'app1', 'script-src') + self.assertIn("'unsafe-eval'", result) + self.assertNotIn("'unsafe-inline'", result) + + def test_get_csp_flags_includes_unsafe_inline(self): + result = get_csp_flags(self.applications, 'app1', 'style-src') + self.assertIn("'unsafe-inline'", result) + self.assertNotIn("'unsafe-eval'", result) + + def test_get_csp_flags_empty_when_none_configured(self): + result = get_csp_flags(self.applications, 'app1', 'connect-src') + self.assertEqual(result, []) + + def test_get_csp_flags_empty_when_app_missing(self): + result = get_csp_flags(self.applications, 'nonexistent_app', 'script-src') + self.assertEqual(result, []) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file