mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-15 08:30:46 +02:00
Added auto setting for redirect urls for keycloak clients. Element and Synapse still need to be mapped
This commit is contained in:
parent
0746acedfd
commit
6e8ae793e3
3
Todo.md
3
Todo.md
@ -1,3 +1,4 @@
|
||||
# Todos
|
||||
- Implement multi language
|
||||
- Implement rbac administration interface
|
||||
- Implement rbac administration interface
|
||||
- Implement ``MASK_CREDENTIALS_IN_LOGS`` for all sensible tasks
|
@ -1,27 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
import os
|
||||
import sys
|
||||
from ansible.errors import AnsibleFilterError
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from module_utils.get_url import get_url
|
||||
|
||||
class FilterModule(object):
|
||||
''' Infinito.Nexus application config extraction filters '''
|
||||
def filters(self):
|
||||
return {'get_url': self.get_url}
|
||||
|
||||
def get_url(self, domains, application_id, protocol):
|
||||
# 1) module_util-Verzeichnis in den Pfad aufnehmen
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
project_root = os.path.dirname(plugin_dir)
|
||||
module_utils = os.path.join(project_root, 'module_utils')
|
||||
if module_utils not in sys.path:
|
||||
sys.path.append(module_utils)
|
||||
|
||||
# 2) jetzt domain_utils importieren
|
||||
try:
|
||||
from domain_utils import get_domain
|
||||
except ImportError as e:
|
||||
raise AnsibleFilterError(f"could not import domain_utils: {e}")
|
||||
|
||||
# 3) Validierung und Aufruf
|
||||
if not isinstance(protocol, str):
|
||||
raise AnsibleFilterError("Protocol must be a string")
|
||||
return f"{protocol}://{ get_domain(domains, application_id) }"
|
||||
return {
|
||||
'get_url': get_url,
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
INFINITO_ENVIRONMENT: "production" # Possible values: production, development
|
||||
|
||||
# If true, sensitive credentials will be masked or hidden from all Ansible task logs
|
||||
# Recommendet to set to true
|
||||
# @todo needs to be implemented everywhere
|
||||
MASK_CREDENTIALS_IN_LOGS: true
|
||||
|
||||
HOST_CURRENCY: "EUR"
|
||||
HOST_TIMEZONE: "UTC"
|
||||
|
||||
|
18
module_utils/get_url.py
Normal file
18
module_utils/get_url.py
Normal file
@ -0,0 +1,18 @@
|
||||
from ansible.errors import AnsibleFilterError
|
||||
import sys, os
|
||||
|
||||
def get_url(domains, application_id, protocol):
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
project_root = os.path.dirname(plugin_dir)
|
||||
module_utils = os.path.join(project_root, 'module_utils')
|
||||
if module_utils not in sys.path:
|
||||
sys.path.append(module_utils)
|
||||
|
||||
try:
|
||||
from domain_utils import get_domain
|
||||
except ImportError as e:
|
||||
raise AnsibleFilterError(f"could not import domain_utils: {e}")
|
||||
|
||||
if not isinstance(protocol, str):
|
||||
raise AnsibleFilterError("Protocol must be a string")
|
||||
return f"{protocol}://{ get_domain(domains, application_id) }"
|
@ -1,4 +1,4 @@
|
||||
import_realm: True # If True realm will be imported. If false skip.
|
||||
import_realm: True # If True realm will be imported. If false skip.
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
@ -6,7 +6,12 @@ features:
|
||||
ldap: true
|
||||
central_database: true
|
||||
recaptcha: true
|
||||
logout: true
|
||||
|
||||
# Doesn't make sense to activate logout page for keycloak, because the logout page
|
||||
# anyhow should be included via iframe in keycloak.
|
||||
# The JS is also messing with the keycloak config fields
|
||||
# @todo optimize the JS
|
||||
logout: false
|
||||
server:
|
||||
csp:
|
||||
flags:
|
||||
|
0
roles/web-app-keycloak/filter_plugins/__init__.py
Normal file
0
roles/web-app-keycloak/filter_plugins/__init__.py
Normal file
82
roles/web-app-keycloak/filter_plugins/redirect_uris.py
Normal file
82
roles/web-app-keycloak/filter_plugins/redirect_uris.py
Normal file
@ -0,0 +1,82 @@
|
||||
# roles/web-app-keycloak/filter_plugins/redirect_uris.py
|
||||
from __future__ import annotations
|
||||
import os, sys
|
||||
from typing import Iterable, Sequence
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
# --- Locate project root that contains `module_utils/` dynamically (up to 5 levels) ---
|
||||
def _ensure_module_utils_on_path():
|
||||
here = os.path.dirname(__file__)
|
||||
for depth in range(1, 6):
|
||||
candidate = os.path.abspath(os.path.join(here, *(['..'] * depth)))
|
||||
if os.path.isdir(os.path.join(candidate, 'module_utils')):
|
||||
if candidate not in sys.path:
|
||||
sys.path.insert(0, candidate)
|
||||
return
|
||||
# If not found, imports below will raise a clear error
|
||||
_ensure_module_utils_on_path()
|
||||
|
||||
# Import your existing helpers
|
||||
from module_utils.config_utils import get_app_conf, AppConfigKeyError, ConfigEntryNotSetError
|
||||
from module_utils.get_url import get_url # returns "<protocol>://<domain>"
|
||||
|
||||
def _stable_dedup(items: Sequence[str]) -> list[str]:
|
||||
seen = set()
|
||||
out: list[str] = []
|
||||
for x in items:
|
||||
if x not in seen:
|
||||
seen.add(x)
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
def redirect_uris(domains: dict,
|
||||
applications: dict,
|
||||
web_protocol: str = "https",
|
||||
wildcard: str = "/*",
|
||||
features: Iterable[str] = ("features.oauth2", "features.oidc"),
|
||||
dedup: bool = True) -> list[str]:
|
||||
"""
|
||||
Build redirect URIs using:
|
||||
- get_app_conf(applications, app_id, dotted_key, default) for feature gating
|
||||
- get_url(domains_subset, app_id, web_protocol) to form "<proto>://<domain>"
|
||||
|
||||
For domain lists, we call get_url() once per domain by passing a minimal
|
||||
per-app subset like {app_id: "example.org"} to preserve your original
|
||||
'one entry per domain' behavior.
|
||||
"""
|
||||
if not isinstance(domains, dict):
|
||||
raise AnsibleFilterError("redirect_uris: 'domains' must be a dict mapping app_id -> domain or list of domains")
|
||||
|
||||
uris: list[str] = []
|
||||
|
||||
for app_id, domain_value in domains.items():
|
||||
# Feature check via get_app_conf
|
||||
try:
|
||||
has_feature = any(bool(get_app_conf(applications, app_id, f, False)) for f in features)
|
||||
except (AppConfigKeyError, ConfigEntryNotSetError):
|
||||
has_feature = False
|
||||
|
||||
if not has_feature:
|
||||
continue
|
||||
|
||||
# Normalize to iterable of domains
|
||||
doms = [domain_value] if isinstance(domain_value, str) else list(domain_value or [])
|
||||
|
||||
for d in doms:
|
||||
# Use get_url() to produce "<proto>://<domain>"
|
||||
# Pass a minimal per-app mapping so get_domain() resolves to 'd'
|
||||
try:
|
||||
url = get_url({app_id: d}, app_id, web_protocol)
|
||||
except Exception as e:
|
||||
raise AnsibleFilterError(f"redirect_uris: get_url failed for app '{app_id}' with domain '{d}': {e}")
|
||||
uris.append(f"{url}{wildcard}")
|
||||
|
||||
return _stable_dedup(uris) if dedup else uris
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
"""Infinito.Nexus redirect URI builder (uses get_app_conf + get_url)"""
|
||||
def filters(self):
|
||||
return {
|
||||
"redirect_uris": redirect_uris,
|
||||
}
|
130
roles/web-app-keycloak/tasks/02_update_client_redirects.yml
Normal file
130
roles/web-app-keycloak/tasks/02_update_client_redirects.yml
Normal file
@ -0,0 +1,130 @@
|
||||
---
|
||||
# Update redirectUris/webOrigins per kcadm.sh — no defaults used.
|
||||
|
||||
# ── REQUIRED VARS (must be provided by caller) ───────────────────────────────
|
||||
# - WEB_PROTOCOL e.g. "https"
|
||||
# - keycloak_realm target realm name
|
||||
# - keycloak_server_host_url e.g. "http://127.0.0.1:8080"
|
||||
# - keycloak_server_internal_url e.g. "http://127.0.0.1:8080"
|
||||
# - keycloak_kcadm_path e.g. "docker exec -i keycloak /opt/keycloak/bin/kcadm.sh"
|
||||
# - keycloak_master_api_user_name
|
||||
# - keycloak_master_api_user_password
|
||||
# - keycloak_client_id clientId to update (e.g. same as realm or an app client)
|
||||
# - domains your domain map
|
||||
# - applications your applications map
|
||||
|
||||
- name: "Assert required variables are present (no defaults allowed)"
|
||||
assert:
|
||||
that:
|
||||
- WEB_PROTOCOL is defined
|
||||
- keycloak_realm is defined
|
||||
- keycloak_server_host_url is defined
|
||||
- keycloak_server_internal_url is defined
|
||||
- keycloak_kcadm_path is defined
|
||||
- keycloak_master_api_user_name is defined
|
||||
- keycloak_master_api_user_password is defined
|
||||
- keycloak_client_id is defined
|
||||
- keycloak_redirect_features is defined
|
||||
- domains is defined
|
||||
- applications is defined
|
||||
fail_msg: "Missing required variable(s). Provide all vars listed at the top of 10_update_client_redirects.yml."
|
||||
|
||||
# 0) Wait & login
|
||||
- name: "Wait until Keycloak is reachable at {{ keycloak_server_host_url }}"
|
||||
uri:
|
||||
url: "{{ keycloak_server_host_url }}/realms/master"
|
||||
method: GET
|
||||
status_code: 200
|
||||
validate_certs: false
|
||||
register: kc_up
|
||||
retries: 30
|
||||
delay: 5
|
||||
until: kc_up.status == 200
|
||||
|
||||
- name: "kcadm login"
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
shell: >
|
||||
{{ keycloak_kcadm_path }} config credentials
|
||||
--server {{ keycloak_server_internal_url }}
|
||||
--realm master
|
||||
--user {{ keycloak_master_api_user_name }}
|
||||
--password {{ keycloak_master_api_user_password }}
|
||||
changed_when: false
|
||||
|
||||
# 1) Build desired sets (NO defaults)
|
||||
- name: "Build desired redirect URIs from config via filter"
|
||||
set_fact:
|
||||
kc_redirect_uris: >-
|
||||
{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', keycloak_redirect_features, True) }}
|
||||
|
||||
- name: Build desired web origins (scheme://host[:port])
|
||||
set_fact:
|
||||
kc_web_origins: >-
|
||||
{{ kc_redirect_uris
|
||||
| map('regex_replace','/\\*$','')
|
||||
| map('regex_search','^(https?://[^/]+)')
|
||||
| select('string')
|
||||
| list | unique }}
|
||||
|
||||
#- name: "Build post.logout.redirect.uris value ('+' plus explicit URIs without /*)"
|
||||
# set_fact:
|
||||
# kc_desired_post_logout_uris: >-
|
||||
# {{ (['+'] + (kc_redirect_uris | map('regex_replace','/\\*$','') | list)) | join('\n') }}
|
||||
|
||||
# 2) Resolve client id (strictly by provided clientId, no fallback)
|
||||
- name: "Resolve client internal id for {{ keycloak_client_id }}"
|
||||
shell: >
|
||||
{{ keycloak_kcadm_path }} get clients
|
||||
-r {{ keycloak_realm }}
|
||||
--query 'clientId={{ keycloak_client_id }}'
|
||||
--fields id --format json | jq -r '.[0].id'
|
||||
register: kc_client
|
||||
changed_when: false
|
||||
|
||||
- name: "Fail if client not found"
|
||||
assert:
|
||||
that: kc_client.stdout is match('^[0-9a-f-]+$')
|
||||
fail_msg: "Client '{{ keycloak_client_id }}' not found in realm '{{ keycloak_realm }}'."
|
||||
|
||||
# 3) Read current config (assume keys exist; we don't use defaults)
|
||||
- name: "Read current client configuration"
|
||||
shell: >
|
||||
{{ keycloak_kcadm_path }} get clients/{{ kc_client.stdout }}
|
||||
-r {{ keycloak_realm }} --format json
|
||||
register: kc_client_obj
|
||||
changed_when: false
|
||||
|
||||
- name: "Normalize current vs desired for comparison"
|
||||
set_fact:
|
||||
kc_current_redirect_uris: "{{ (kc_client_obj.stdout | from_json).redirectUris | sort }}"
|
||||
kc_current_web_origins: "{{ (kc_client_obj.stdout | from_json).webOrigins | sort }}"
|
||||
kc_current_logout_uris: >-
|
||||
{{
|
||||
(
|
||||
(kc_client_obj.stdout | from_json).attributes['post.logout.redirect.uris']
|
||||
if 'post.logout.redirect.uris' in (kc_client_obj.stdout | from_json).attributes
|
||||
else ''
|
||||
)
|
||||
| regex_replace('\r','')
|
||||
| split('\n')
|
||||
| reject('equalto','')
|
||||
| list | sort
|
||||
}}
|
||||
kc_desired_redirect_uris: "{{ kc_redirect_uris | sort }}"
|
||||
kc_desired_web_origins: "{{ kc_web_origins | sort }}"
|
||||
kc_desired_post_logout_uris: "+"
|
||||
kc_desired_post_logout_uris_list: >-
|
||||
{{ "+" | split('\n') | reject('equalto','') | list | sort }}
|
||||
|
||||
# 4) Update only when changed
|
||||
- name: "Update redirectUris, webOrigins, post.logout.redirect.uris"
|
||||
shell: >
|
||||
{{ keycloak_kcadm_path }} update clients/{{ kc_client.stdout }}
|
||||
-r {{ keycloak_realm }}
|
||||
-s 'redirectUris={{ kc_redirect_uris | to_json }}'
|
||||
-s 'webOrigins={{ kc_web_origins | to_json }}'
|
||||
-s 'attributes."post.logout.redirect.uris"={{ kc_desired_post_logout_uris | to_json }}'
|
||||
when: kc_current_redirect_uris != kc_desired_redirect_uris
|
||||
or kc_current_web_origins != kc_desired_web_origins
|
||||
or kc_current_logout_uris != kc_desired_post_logout_uris_list
|
||||
|
@ -16,8 +16,8 @@
|
||||
{{ keycloak_kcadm_path }} config credentials \
|
||||
--server {{ keycloak_server_internal_url }} \
|
||||
--realm master \
|
||||
--user {{ keycloak_administrator_username }} \
|
||||
--password {{ keycloak_administrator_password }}
|
||||
--user {{ keycloak_master_api_user_name }} \
|
||||
--password {{ keycloak_master_api_user_password }}
|
||||
|
||||
- name: Retrieve LDAP component ID
|
||||
shell: |
|
||||
@ -37,6 +37,6 @@
|
||||
{{ keycloak_kcadm_path }} update components/{{ ldap_component.stdout }} \
|
||||
-r {{ keycloak_realm }} \
|
||||
-s 'config.bindCredential=["{{ new_bind_password }}"]'
|
||||
no_log: true
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
register: update_bind
|
||||
changed_when: update_bind.rc == 0
|
@ -15,8 +15,8 @@
|
||||
{{ keycloak_kcadm_path }} config credentials \
|
||||
--server {{ keycloak_server_internal_url }} \
|
||||
--realm master \
|
||||
--user {{ keycloak_administrator_username }} \
|
||||
--password {{ keycloak_administrator_password }}
|
||||
--user {{ keycloak_master_api_user_name }} \
|
||||
--password {{ keycloak_master_api_user_password }}
|
||||
|
||||
# LDAP Source
|
||||
- name: Get ID of LDAP storage provider
|
||||
@ -66,8 +66,8 @@
|
||||
{{ keycloak_kcadm_path }} config credentials \
|
||||
--server {{ keycloak_server_internal_url }} \
|
||||
--realm master \
|
||||
--user {{ keycloak_administrator_username }} \
|
||||
--password {{ keycloak_administrator_password }}
|
||||
--user {{ keycloak_master_api_user_name }} \
|
||||
--password {{ keycloak_master_api_user_password }}
|
||||
|
||||
- name: Render user-profile JSON for SSH key
|
||||
template:
|
3
roles/web-app-keycloak/tasks/Todo.md
Normal file
3
roles/web-app-keycloak/tasks/Todo.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Todos
|
||||
- Include 03_update-ldap-bind.yml
|
||||
- Include 04_ssh_public_key.yml
|
@ -1,11 +1,14 @@
|
||||
---
|
||||
- name: "create import files for {{application_id}}"
|
||||
include_tasks: 01_import.yml
|
||||
#- name: "create import files for {{application_id}}"
|
||||
# include_tasks: 01_import.yml
|
||||
#
|
||||
#- name: "load docker, db and proxy for {{application_id}}"
|
||||
# include_role:
|
||||
# name: cmp-db-docker-proxy
|
||||
|
||||
- name: "load docker, db and proxy for {{application_id}}"
|
||||
include_role:
|
||||
name: cmp-db-docker-proxy
|
||||
- name: "Apply client redirects without realm import"
|
||||
include_tasks: 02_update_client_redirects.yml
|
||||
|
||||
# Deactivated temporary. Import now via realm.yml
|
||||
#- name: Implement SSH Public Key Attribut
|
||||
# include_tasks: attributes/ssh_public_key.yml
|
||||
# include_tasks: 03_ssh_public_key.yml
|
@ -833,20 +833,8 @@
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "desktop-secret",
|
||||
"secret": "{{oidc.client.secret}}",
|
||||
{%- set redirect_uris = [] %}
|
||||
{%- for domain_application_id, domain in domains.items() %}
|
||||
{%- if applications | get_app_conf(domain_application_id, 'features.oauth2', False) or applications | get_app_conf(domain_application_id, 'features.oidc', False) %}
|
||||
{%- if domain is string %}
|
||||
{%- set _ = redirect_uris.append(WEB_PROTOCOL ~ '://' ~ domain ~ '/*') %}
|
||||
{%- else %}
|
||||
{%- for d in domain %}
|
||||
{%- set _ = redirect_uris.append(WEB_PROTOCOL ~ '://' ~ d ~ '/*') %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
"redirectUris": {{ redirect_uris | tojson }},
|
||||
{# The following line should be covered by 02_update_client_redirects.yml #}
|
||||
"redirectUris": {{ domains | redirect_uris(applications, WEB_PROTOCOL) | tojson }},
|
||||
"webOrigins": [
|
||||
"{{ WEB_PROTOCOL }}://*.{{primary_domain}}"
|
||||
],
|
||||
|
@ -6,9 +6,9 @@ database_type: "postgres"
|
||||
keycloak_container: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name', True) }}" # Name of the keycloack docker container
|
||||
keycloak_docker_import_directory: "/opt/keycloak/data/import/" # Directory in which keycloack import files are placed in the running docker container
|
||||
keycloak_realm: "{{ primary_domain}}" # This is the name of the default realm which is used by the applications
|
||||
keycloak_administrator: "{{ applications | get_app_conf(application_id, 'users.administrator', True) }}" # Master Administrator
|
||||
keycloak_administrator_username: "{{ keycloak_administrator.username }}" # Master Administrator Username
|
||||
keycloak_administrator_password: "{{ keycloak_administrator.password }}" # Master Administrator Password
|
||||
keycloak_master_api_user: "{{ applications | get_app_conf(application_id, 'users.administrator', True) }}" # Master Administrator
|
||||
keycloak_master_api_user_name: "{{ keycloak_master_api_user.username }}" # Master Administrator Username
|
||||
keycloak_master_api_user_password: "{{ keycloak_master_api_user.password }}" # Master Administrator Password
|
||||
keycloak_kcadm_path: "docker exec -i {{ keycloak_container }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
|
||||
keycloak_server_internal_url: "http://127.0.0.1:8080"
|
||||
keycloak_server_host: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
|
||||
@ -17,6 +17,8 @@ keycloak_image: "{{ applications | get_app_conf(application_id
|
||||
keycloak_version: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version', True) }}" # Keyloak docker version
|
||||
keycloak_import_realm: "{{ applications | get_app_conf(application_id, 'import_realm', True, True) }}" # Activate realm import
|
||||
keycloak_debug_enabled: "{{ enable_debug }}"
|
||||
keycloak_redirect_features: ["features.oauth2","features.oidc"]
|
||||
keycloak_client_id: "{{ oidc.client.id }}"
|
||||
|
||||
# Docker
|
||||
docker_compose_flush_handlers: true # Remember to copy realm import before flushg when set to true
|
0
tests/unit/roles/web-app-keycloak/__init__.py
Normal file
0
tests/unit/roles/web-app-keycloak/__init__.py
Normal file
@ -0,0 +1,159 @@
|
||||
# tests/unit/roles/web-app-keycloak/filter_plugins/test_redirect_uris.py
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
import importlib.util
|
||||
|
||||
PLUGIN_REL_PATH = os.path.join("roles", "web-app-keycloak", "filter_plugins", "redirect_uris.py")
|
||||
|
||||
|
||||
def _find_repo_root_containing(rel_path, max_depth=8):
|
||||
"""Walk upwards from this test file to find the repo root that contains rel_path."""
|
||||
here = os.path.dirname(__file__)
|
||||
cur = here
|
||||
for _ in range(max_depth):
|
||||
candidate = os.path.join(cur, rel_path)
|
||||
if os.path.isfile(candidate):
|
||||
return cur
|
||||
parent = os.path.dirname(cur)
|
||||
if parent == cur:
|
||||
break
|
||||
cur = parent
|
||||
raise FileNotFoundError(f"Could not find {rel_path} upwards from {here}")
|
||||
|
||||
|
||||
def _load_module_from_path(name, file_path):
|
||||
spec = importlib.util.spec_from_file_location(name, file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader, f"Cannot load spec for {file_path}"
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class RedirectUrisTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Create stub package: module_utils, with config_utils and get_url submodules.
|
||||
mu = types.ModuleType("module_utils")
|
||||
mu_config = types.ModuleType("module_utils.config_utils")
|
||||
mu_geturl = types.ModuleType("module_utils.get_url")
|
||||
|
||||
# Define stub exceptions
|
||||
class AppConfigKeyError(Exception):
|
||||
pass
|
||||
|
||||
class ConfigEntryNotSetError(Exception):
|
||||
pass
|
||||
|
||||
# Define a practical get_app_conf that understands dotted keys
|
||||
def get_app_conf(applications, app_id, dotted, default=None):
|
||||
data = applications.get(app_id, {})
|
||||
cur = data
|
||||
for part in dotted.split("."):
|
||||
if isinstance(cur, dict) and part in cur:
|
||||
cur = cur[part]
|
||||
else:
|
||||
return default
|
||||
return cur
|
||||
|
||||
# Define a simple get_url matching your module_utils/get_url contract
|
||||
# get_url(domains, application_id, protocol) -> "<protocol>://<domain>"
|
||||
def get_url(domains, application_id, protocol):
|
||||
domain = domains[application_id]
|
||||
return f"{protocol}://{domain}"
|
||||
|
||||
# Attach to stub modules
|
||||
mu_config.get_app_conf = staticmethod(get_app_conf)
|
||||
mu_config.AppConfigKeyError = AppConfigKeyError
|
||||
mu_config.ConfigEntryNotSetError = ConfigEntryNotSetError
|
||||
|
||||
mu_geturl.get_url = staticmethod(get_url)
|
||||
|
||||
# Register in sys.modules so plugin imports succeed
|
||||
sys.modules["module_utils"] = mu
|
||||
sys.modules["module_utils.config_utils"] = mu_config
|
||||
sys.modules["module_utils.get_url"] = mu_geturl
|
||||
|
||||
# Load the plugin by path
|
||||
repo_root = _find_repo_root_containing(PLUGIN_REL_PATH)
|
||||
plugin_path = os.path.join(repo_root, PLUGIN_REL_PATH)
|
||||
cls.plugin = _load_module_from_path("test_target.redirect_uris", plugin_path)
|
||||
|
||||
# Keep originals for per-test monkeypatching
|
||||
cls._orig_get_app_conf = cls.plugin.get_app_conf
|
||||
cls._orig_get_url = cls.plugin.get_url
|
||||
|
||||
def tearDown(self):
|
||||
# Restore plugin functions if a test monkeypatched them
|
||||
self.plugin.get_app_conf = self._orig_get_app_conf
|
||||
self.plugin.get_url = self._orig_get_url
|
||||
|
||||
def test_single_domain_oauth2_enabled(self):
|
||||
domains = {"app1": "example.org"}
|
||||
applications = {"app1": {"features": {"oauth2": True}}}
|
||||
result = self.plugin.redirect_uris(domains, applications, web_protocol="https")
|
||||
self.assertEqual(result, ["https://example.org/*"])
|
||||
|
||||
def test_multiple_domains_oidc_enabled(self):
|
||||
domains = {"appX": ["a.example.org", "b.example.org"]}
|
||||
applications = {"appX": {"features": {"oidc": True}}}
|
||||
result = self.plugin.redirect_uris(domains, applications, web_protocol="https")
|
||||
self.assertCountEqual(result, ["https://a.example.org/*", "https://b.example.org/*"])
|
||||
|
||||
def test_feature_missing_is_skipped(self):
|
||||
domains = {"app1": "example.org"}
|
||||
applications = {"app1": {"features": {"oauth2": False, "oidc": False}}}
|
||||
result = self.plugin.redirect_uris(domains, applications)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_protocol_and_wildcard_customization(self):
|
||||
domains = {"app1": "x.test"}
|
||||
applications = {"app1": {"features": {"oauth2": True}}}
|
||||
result = self.plugin.redirect_uris(domains, applications, web_protocol="http", wildcard="/cb")
|
||||
self.assertEqual(result, ["http://x.test/cb"])
|
||||
|
||||
def test_dedup_default_true(self):
|
||||
domains = {"app1": ["dup.test", "dup.test", "other.test"]}
|
||||
applications = {"app1": {"features": {"oidc": True}}}
|
||||
result = self.plugin.redirect_uris(domains, applications)
|
||||
self.assertEqual(result, ["https://dup.test/*", "https://other.test/*"])
|
||||
|
||||
def test_dedup_false_keeps_duplicates(self):
|
||||
domains = {"app1": ["dup.test", "dup.test"]}
|
||||
applications = {"app1": {"features": {"oidc": True}}}
|
||||
result = self.plugin.redirect_uris(domains, applications, dedup=False)
|
||||
self.assertEqual(result, ["https://dup.test/*", "https://dup.test/*"])
|
||||
|
||||
def test_invalid_domains_type_raises(self):
|
||||
with self.assertRaises(self.plugin.AnsibleFilterError):
|
||||
self.plugin.redirect_uris(["not-a-dict"], {}) # type: ignore[arg-type]
|
||||
|
||||
def test_get_url_failure_is_wrapped(self):
|
||||
# Make get_url raise an arbitrary error; plugin should re-raise AnsibleFilterError
|
||||
def boom(*args, **kwargs):
|
||||
raise RuntimeError("boom")
|
||||
self.plugin.get_url = boom
|
||||
|
||||
domains = {"app1": "example.org"}
|
||||
applications = {"app1": {"features": {"oauth2": True}}}
|
||||
|
||||
with self.assertRaises(self.plugin.AnsibleFilterError) as ctx:
|
||||
self.plugin.redirect_uris(domains, applications)
|
||||
self.assertIn("get_url failed", str(ctx.exception))
|
||||
|
||||
def test_get_app_conf_exception_is_handled_as_no_feature(self):
|
||||
# Make get_app_conf raise AppConfigKeyError; plugin should treat as not enabled and skip
|
||||
def raising_get_app_conf(*args, **kwargs):
|
||||
raise self.plugin.AppConfigKeyError("missing key")
|
||||
self.plugin.get_app_conf = raising_get_app_conf
|
||||
|
||||
domains = {"app1": "example.org"}
|
||||
applications = {"app1": {"features": {"oauth2": True}}} # value won't be read due to exception
|
||||
|
||||
result = self.plugin.redirect_uris(domains, applications)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user