mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
The realm carried exactly one client, the shared OIDC one, and no SAML client existed anywhere in the repo - `flavor: saml` was in the resolver's enum with zero consumers. Apps that cannot speak OIDC therefore had no way to authenticate against Keycloak at all; they could only be fronted by the oauth2-proxy, which authenticates at the edge and leaves the app itself without a session. KEYCLOAK_SAML_APPS collects every deployed app whose services.sso.flavor is saml. It filters stepwise - value.services, then .sso, then .flavor, each guarded by `defined` - because Ansible's undefined is a Marker(StrictUndefined, Tripwire): resolving that chain in one selectattr raises for every app without an sso block instead of yielding undefined, which would have broken the realm render for every deployment in the repo. The include is unconditional and the loop emits a leading comma per client, so the realm needs no branch on the list being empty. That matters: an Ansible `>-` value that failed to convert would be a non-empty string and a truthiness guard would then splice a broken clients array. The client's shape is not guesswork - every attribute comes from a failure reproduced against a live Keycloak 26.7.1 until a SuiteCRM SAML login completed: defaultClientScopes is emptied because the built-in role_list scope emits one Role attribute per realm role and the SP rejects the assertion with "Found an Attribute element with duplicated Name"; saml.client.signature is false because the SP does not sign its AuthnRequests; the username mapper's attribute name comes from OIDC.ATTRIBUTES.USERNAME rather than a hardcoded claim. The new test renders the complete realm - every include, the role's own ldap_groups_filter, StrictUndefined - and parses it as JSON for zero, one and several SAML apps. The file had no test before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
66 lines
2.1 KiB
Django/Jinja
66 lines
2.1 KiB
Django/Jinja
{% for app in KEYCLOAK_SAML_APPS %}
|
|
{% set app_url = lookup('tls', app, 'url.base') %}
|
|
,{
|
|
"clientId": "{{ [app_url, 'saml/metadata'] | path_join }}",
|
|
"name": "{{ app }}",
|
|
"enabled": true,
|
|
"protocol": "saml",
|
|
"frontchannelLogout": true,
|
|
"redirectUris": ["{{ [app_url, 'saml/acs'] | path_join }}"],
|
|
"defaultClientScopes": [],
|
|
"optionalClientScopes": [],
|
|
"attributes": {
|
|
"saml.assertion.signature": "true",
|
|
"saml.server.signature": "true",
|
|
"saml.client.signature": "false",
|
|
"saml.signature.algorithm": "RSA_SHA256",
|
|
"saml_name_id_format": "username",
|
|
"saml_force_name_id_format": "true",
|
|
"saml_assertion_consumer_url_post": "{{ [app_url, 'saml/acs'] | path_join }}",
|
|
"saml_single_logout_service_url_redirect": "{{ [app_url, 'saml/logout'] | path_join }}"
|
|
},
|
|
"protocolMappers": [
|
|
{
|
|
"name": "username",
|
|
"protocol": "saml",
|
|
"protocolMapper": "saml-user-property-mapper",
|
|
"config": {
|
|
"user.attribute": "username",
|
|
"attribute.name": "{{ OIDC.ATTRIBUTES.USERNAME }}",
|
|
"attribute.nameformat": "Basic"
|
|
}
|
|
},
|
|
{
|
|
"name": "email",
|
|
"protocol": "saml",
|
|
"protocolMapper": "saml-user-property-mapper",
|
|
"config": {
|
|
"user.attribute": "email",
|
|
"attribute.name": "email",
|
|
"attribute.nameformat": "Basic"
|
|
}
|
|
},
|
|
{
|
|
"name": "firstName",
|
|
"protocol": "saml",
|
|
"protocolMapper": "saml-user-property-mapper",
|
|
"config": {
|
|
"user.attribute": "firstName",
|
|
"attribute.name": "firstName",
|
|
"attribute.nameformat": "Basic"
|
|
}
|
|
},
|
|
{
|
|
"name": "lastName",
|
|
"protocol": "saml",
|
|
"protocolMapper": "saml-user-property-mapper",
|
|
"config": {
|
|
"user.attribute": "lastName",
|
|
"attribute.name": "lastName",
|
|
"attribute.nameformat": "Basic"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
{% endfor %}
|