1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-03-22 05:09:12 +00:00

keycloak_client: add valid_post_logout_redirect_uris and backchannel_logout_url (#11473)

* feat(keycloak_client): add valid_post_logout_redirect_uris and backchannel_logout_url

Add two new convenience parameters that map to client attributes:

- valid_post_logout_redirect_uris: sets post.logout.redirect.uris
  attribute (list items joined with ##)
- backchannel_logout_url: sets backchannel.logout.url attribute

These fields are not top-level in the Keycloak REST API but are stored
as client attributes. The new parameters provide a user-friendly
interface without requiring users to know the internal attribute names
and ##-separator format.

Fixes #6812, fixes #4892

* consolidate changelog and add PR link per review feedback
This commit is contained in:
Ivan Kokalovic 2026-02-07 16:19:29 +01:00 committed by GitHub
parent c41de53dbb
commit df6d6269a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 138 additions and 1 deletions

View file

@ -151,6 +151,17 @@ options:
type: list
elements: str
valid_post_logout_redirect_uris:
description:
- Valid post logout redirect URIs for this client.
- This is stored as C(post.logout.redirect.uris) in the client attributes.
- Use V(+) as a single list element to allow all redirect URIs.
aliases:
- postLogoutRedirectUris
type: list
elements: str
version_added: "12.4.0"
not_before:
description:
- Revoke any tokens issued before this date for this client (this is a UNIX timestamp). This is C(notBefore) in the
@ -227,6 +238,15 @@ options:
- frontchannelLogout
type: bool
backchannel_logout_url:
description:
- URL that will cause the client to log itself out when a logout request is sent to this realm.
- This is stored as C(backchannel.logout.url) in the client attributes.
aliases:
- backchannelLogoutUrl
type: str
version_added: "12.4.0"
protocol:
description:
- Type of client.
@ -761,6 +781,21 @@ PROTOCOL_SAML = "saml"
PROTOCOL_DOCKER_V2 = "docker-v2"
CLIENT_META_DATA = ["authorizationServicesEnabled"]
# Parameters that map to client attributes rather than top-level API fields.
# Each entry maps the module parameter name to (attribute_key, transform_fn).
# transform_fn converts the module param value to the attribute string value.
# Use None for transform_fn when no transformation is needed (identity).
ATTRIBUTE_PARAMS = {
"valid_post_logout_redirect_uris": (
"post.logout.redirect.uris",
"##".join,
),
"backchannel_logout_url": (
"backchannel.logout.url",
None,
),
}
def normalise_scopes_for_behavior(desired_client, before_client, clientScopesBehavior):
"""
@ -1219,6 +1254,7 @@ def main():
default_roles=dict(type="list", elements="str", aliases=["defaultRoles"]),
redirect_uris=dict(type="list", elements="str", aliases=["redirectUris"]),
web_origins=dict(type="list", elements="str", aliases=["webOrigins"]),
valid_post_logout_redirect_uris=dict(type="list", elements="str", aliases=["postLogoutRedirectUris"]),
not_before=dict(type="int", aliases=["notBefore"]),
bearer_only=dict(type="bool", aliases=["bearerOnly"]),
consent_required=dict(type="bool", aliases=["consentRequired"]),
@ -1229,6 +1265,7 @@ def main():
authorization_services_enabled=dict(type="bool", aliases=["authorizationServicesEnabled"]),
public_client=dict(type="bool", aliases=["publicClient"]),
frontchannel_logout=dict(type="bool", aliases=["frontchannelLogout"]),
backchannel_logout_url=dict(type="str", aliases=["backchannelLogoutUrl"]),
protocol=dict(type="str", choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]),
attributes=dict(type="dict"),
full_scope_allowed=dict(type="bool", aliases=["fullScopeAllowed"]),
@ -1308,9 +1345,20 @@ def main():
# Build a proposed changeset from parameters given to this module
changeset = {}
# Collect attribute-mapped parameters to inject into attributes later
attribute_overrides = {}
for param_name, (attr_key, transform_fn) in ATTRIBUTE_PARAMS.items():
param_value = module.params.get(param_name)
if param_value is not None:
attribute_overrides[attr_key] = transform_fn(param_value) if transform_fn else param_value
for client_param in client_params:
new_param_value = module.params.get(client_param)
# Skip attribute-mapped params; they are handled via attributes
if client_param in ATTRIBUTE_PARAMS:
continue
# Unfortunately, the ansible argument spec checker introduces variables with null values when
# they are not specified
if client_param == "protocol_mappers":
@ -1330,6 +1378,13 @@ def main():
changeset[camel(client_param)] = new_param_value
# Inject attribute-mapped parameters into the attributes dict
if attribute_overrides:
if "attributes" not in changeset:
changeset["attributes"] = copy.deepcopy(before_client.get("attributes", {}))
if isinstance(changeset["attributes"], dict):
changeset["attributes"].update(attribute_overrides)
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
desired_client = copy.deepcopy(before_client)
desired_client.update(changeset)