mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-03-21 20:59:10 +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:
parent
c41de53dbb
commit
df6d6269a6
4 changed files with 138 additions and 1 deletions
|
|
@ -0,0 +1,7 @@
|
||||||
|
minor_changes:
|
||||||
|
- keycloak_client - add ``valid_post_logout_redirect_uris`` option to configure
|
||||||
|
post logout redirect URIs for a client, and ``backchannel_logout_url`` option to configure the
|
||||||
|
backchannel logout URL for a client
|
||||||
|
(https://github.com/ansible-collections/community.general/issues/6812,
|
||||||
|
https://github.com/ansible-collections/community.general/issues/4892,
|
||||||
|
https://github.com/ansible-collections/community.general/pull/11473).
|
||||||
|
|
@ -151,6 +151,17 @@ options:
|
||||||
type: list
|
type: list
|
||||||
elements: str
|
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:
|
not_before:
|
||||||
description:
|
description:
|
||||||
- Revoke any tokens issued before this date for this client (this is a UNIX timestamp). This is C(notBefore) in the
|
- 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
|
- frontchannelLogout
|
||||||
type: bool
|
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:
|
protocol:
|
||||||
description:
|
description:
|
||||||
- Type of client.
|
- Type of client.
|
||||||
|
|
@ -761,6 +781,21 @@ PROTOCOL_SAML = "saml"
|
||||||
PROTOCOL_DOCKER_V2 = "docker-v2"
|
PROTOCOL_DOCKER_V2 = "docker-v2"
|
||||||
CLIENT_META_DATA = ["authorizationServicesEnabled"]
|
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):
|
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"]),
|
default_roles=dict(type="list", elements="str", aliases=["defaultRoles"]),
|
||||||
redirect_uris=dict(type="list", elements="str", aliases=["redirectUris"]),
|
redirect_uris=dict(type="list", elements="str", aliases=["redirectUris"]),
|
||||||
web_origins=dict(type="list", elements="str", aliases=["webOrigins"]),
|
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"]),
|
not_before=dict(type="int", aliases=["notBefore"]),
|
||||||
bearer_only=dict(type="bool", aliases=["bearerOnly"]),
|
bearer_only=dict(type="bool", aliases=["bearerOnly"]),
|
||||||
consent_required=dict(type="bool", aliases=["consentRequired"]),
|
consent_required=dict(type="bool", aliases=["consentRequired"]),
|
||||||
|
|
@ -1229,6 +1265,7 @@ def main():
|
||||||
authorization_services_enabled=dict(type="bool", aliases=["authorizationServicesEnabled"]),
|
authorization_services_enabled=dict(type="bool", aliases=["authorizationServicesEnabled"]),
|
||||||
public_client=dict(type="bool", aliases=["publicClient"]),
|
public_client=dict(type="bool", aliases=["publicClient"]),
|
||||||
frontchannel_logout=dict(type="bool", aliases=["frontchannelLogout"]),
|
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]),
|
protocol=dict(type="str", choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]),
|
||||||
attributes=dict(type="dict"),
|
attributes=dict(type="dict"),
|
||||||
full_scope_allowed=dict(type="bool", aliases=["fullScopeAllowed"]),
|
full_scope_allowed=dict(type="bool", aliases=["fullScopeAllowed"]),
|
||||||
|
|
@ -1308,9 +1345,20 @@ def main():
|
||||||
# Build a proposed changeset from parameters given to this module
|
# Build a proposed changeset from parameters given to this module
|
||||||
changeset = {}
|
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:
|
for client_param in client_params:
|
||||||
new_param_value = module.params.get(client_param)
|
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
|
# Unfortunately, the ansible argument spec checker introduces variables with null values when
|
||||||
# they are not specified
|
# they are not specified
|
||||||
if client_param == "protocol_mappers":
|
if client_param == "protocol_mappers":
|
||||||
|
|
@ -1330,6 +1378,13 @@ def main():
|
||||||
|
|
||||||
changeset[camel(client_param)] = new_param_value
|
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)
|
# 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 = copy.deepcopy(before_client)
|
||||||
desired_client.update(changeset)
|
desired_client.update(changeset)
|
||||||
|
|
|
||||||
|
|
@ -456,4 +456,74 @@
|
||||||
- end_state.attributes["backchannel.logout.session.required"] == 'false'
|
- end_state.attributes["backchannel.logout.session.required"] == 'false'
|
||||||
- end_state.attributes["oauth2.device.authorization.grant.enabled"] == 'false'
|
- end_state.attributes["oauth2.device.authorization.grant.enabled"] == 'false'
|
||||||
vars:
|
vars:
|
||||||
end_state: "{{ check_client_when_present_and_attributes_modified.end_state }}"
|
end_state: "{{ check_client_when_present_and_attributes_modified.end_state }}"
|
||||||
|
|
||||||
|
# ---- Tests for valid_post_logout_redirect_uris and backchannel_logout_url ----
|
||||||
|
|
||||||
|
- name: Create client with post logout redirect URIs and backchannel logout URL
|
||||||
|
community.general.keycloak_client:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: logout-test-client
|
||||||
|
valid_post_logout_redirect_uris: "{{ post_logout_redirect_uris }}"
|
||||||
|
backchannel_logout_url: "{{ backchannel_logout_url }}"
|
||||||
|
state: present
|
||||||
|
register: result_create_logout_client
|
||||||
|
|
||||||
|
- name: Assert logout client is created with correct attributes
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result_create_logout_client is changed
|
||||||
|
- result_create_logout_client.end_state.attributes["post.logout.redirect.uris"] is defined
|
||||||
|
- result_create_logout_client.end_state.attributes["backchannel.logout.url"] == backchannel_logout_url
|
||||||
|
|
||||||
|
- name: Re-create client with same logout fields (idempotency)
|
||||||
|
community.general.keycloak_client:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: logout-test-client
|
||||||
|
valid_post_logout_redirect_uris: "{{ post_logout_redirect_uris }}"
|
||||||
|
backchannel_logout_url: "{{ backchannel_logout_url }}"
|
||||||
|
state: present
|
||||||
|
register: result_idempotent_logout_client
|
||||||
|
|
||||||
|
- name: Assert logout client is idempotent
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result_idempotent_logout_client is not changed
|
||||||
|
|
||||||
|
- name: Update client logout fields
|
||||||
|
community.general.keycloak_client:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: logout-test-client
|
||||||
|
valid_post_logout_redirect_uris:
|
||||||
|
- "https://example.com/new-logout"
|
||||||
|
backchannel_logout_url: "https://example.com/new-backchannel"
|
||||||
|
state: present
|
||||||
|
register: result_update_logout_client
|
||||||
|
|
||||||
|
- name: Assert logout client fields are updated
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result_update_logout_client is changed
|
||||||
|
- result_update_logout_client.end_state.attributes["backchannel.logout.url"] == "https://example.com/new-backchannel"
|
||||||
|
|
||||||
|
- name: Delete logout test client
|
||||||
|
community.general.keycloak_client:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: logout-test-client
|
||||||
|
state: absent
|
||||||
|
|
@ -20,6 +20,11 @@ auth_args:
|
||||||
auth_username: "{{ admin_user }}"
|
auth_username: "{{ admin_user }}"
|
||||||
auth_password: "{{ admin_password }}"
|
auth_password: "{{ admin_password }}"
|
||||||
|
|
||||||
|
post_logout_redirect_uris:
|
||||||
|
- "https://example.com/logout-callback"
|
||||||
|
- "https://example.com/signout"
|
||||||
|
backchannel_logout_url: "https://example.com/backchannel-logout"
|
||||||
|
|
||||||
redirect_uris1:
|
redirect_uris1:
|
||||||
- "http://example.c.com/"
|
- "http://example.c.com/"
|
||||||
- "http://example.b.com/"
|
- "http://example.b.com/"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue