mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Added auto setting for redirect urls for keycloak clients. Element and Synapse still need to be mapped
This commit is contained in:
@@ -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
|
Reference in New Issue
Block a user