update keycloak modules

This commit is contained in:
Guido Grazioli 2024-10-14 10:07:44 +02:00
commit c6bb815979
4 changed files with 1605 additions and 289 deletions

View file

@ -40,8 +40,8 @@ options:
state:
description:
- State of the client
- On C(present), the client will be created (or updated if it exists already).
- On C(absent), the client will be removed if it exists
- On V(present), the client will be created (or updated if it exists already).
- On V(absent), the client will be removed if it exists
choices: ['present', 'absent']
default: 'present'
type: str
@ -55,7 +55,7 @@ options:
client_id:
description:
- Client id of client to be worked on. This is usually an alphanumeric name chosen by
you. Either this or I(id) is required. If you specify both, I(id) takes precedence.
you. Either this or O(id) is required. If you specify both, O(id) takes precedence.
This is 'clientId' in the Keycloak REST API.
aliases:
- clientId
@ -63,13 +63,13 @@ options:
id:
description:
- Id of client to be worked on. This is usually an UUID. Either this or I(client_id)
- Id of client to be worked on. This is usually an UUID. Either this or O(client_id)
is required. If you specify both, this takes precedence.
type: str
name:
description:
- Name of the client (this is not the same as I(client_id)).
- Name of the client (this is not the same as O(client_id)).
type: str
description:
@ -108,20 +108,21 @@ options:
client_authenticator_type:
description:
- How do clients authenticate with the auth server? Either C(client-secret) or
C(client-jwt) can be chosen. When using C(client-secret), the module parameter
I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url),
C(jwks.url), and C(jwt.credential.certificate) in the I(attributes) module parameter
to configure its behavior.
This is 'clientAuthenticatorType' in the Keycloak REST API.
choices: ['client-secret', 'client-jwt']
- How do clients authenticate with the auth server? Either V(client-secret),
V(client-jwt), or V(client-x509) can be chosen. When using V(client-secret), the module parameter
O(secret) can set it, for V(client-jwt), you can use the keys C(use.jwks.url),
C(jwks.url), and C(jwt.credential.certificate) in the O(attributes) module parameter
to configure its behavior. For V(client-x509) you can use the keys C(x509.allow.regex.pattern.comparison)
and C(x509.subjectdn) in the O(attributes) module parameter to configure which certificate(s) to accept.
- This is 'clientAuthenticatorType' in the Keycloak REST API.
choices: ['client-secret', 'client-jwt', 'client-x509']
aliases:
- clientAuthenticatorType
type: str
secret:
description:
- When using I(client_authenticator_type) C(client-secret) (the default), you can
- When using O(client_authenticator_type=client-secret) (the default), you can
specify a secret here (otherwise one will be generated if it does not exit). If
changing this secret, the module will not register a change currently (but the
changed secret will be saved).
@ -246,9 +247,11 @@ options:
protocol:
description:
- Type of client (either C(openid-connect) or C(saml).
- Type of client.
- At creation only, default value will be V(openid-connect) if O(protocol) is omitted.
- The V(docker-v2) value was added in community.general 8.6.0.
type: str
choices: ['openid-connect', 'saml']
choices: ['openid-connect', 'saml', 'docker-v2']
full_scope_allowed:
description:
@ -286,7 +289,7 @@ options:
use_template_config:
description:
- Whether or not to use configuration from the I(client_template).
- Whether or not to use configuration from the O(client_template).
This is 'useTemplateConfig' in the Keycloak REST API.
aliases:
- useTemplateConfig
@ -294,7 +297,7 @@ options:
use_template_scope:
description:
- Whether or not to use scope configuration from the I(client_template).
- Whether or not to use scope configuration from the O(client_template).
This is 'useTemplateScope' in the Keycloak REST API.
aliases:
- useTemplateScope
@ -302,7 +305,7 @@ options:
use_template_mappers:
description:
- Whether or not to use mapper configuration from the I(client_template).
- Whether or not to use mapper configuration from the O(client_template).
This is 'useTemplateMappers' in the Keycloak REST API.
aliases:
- useTemplateMappers
@ -338,6 +341,42 @@ options:
description:
- Override realm authentication flow bindings.
type: dict
suboptions:
browser:
description:
- Flow ID of the browser authentication flow.
- O(authentication_flow_binding_overrides.browser)
and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive.
type: str
browser_name:
description:
- Flow name of the browser authentication flow.
- O(authentication_flow_binding_overrides.browser)
and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive.
aliases:
- browserName
type: str
version_added: 9.1.0
direct_grant:
description:
- Flow ID of the direct grant authentication flow.
- O(authentication_flow_binding_overrides.direct_grant)
and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive.
aliases:
- directGrant
type: str
direct_grant_name:
description:
- Flow name of the direct grant authentication flow.
- O(authentication_flow_binding_overrides.direct_grant)
and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive.
aliases:
- directGrantName
type: str
version_added: 9.1.0
aliases:
- authenticationFlowBindingOverrides
version_added: 3.4.0
@ -391,38 +430,37 @@ options:
protocol:
description:
- This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper.
is active.
choices: ['openid-connect', 'saml']
- This specifies for which protocol this protocol mapper is active.
choices: ['openid-connect', 'saml', 'docker-v2']
type: str
protocolMapper:
description:
- The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
- "The Keycloak-internal name of the type of this protocol-mapper. While an exhaustive list is
impossible to provide since this may be extended through SPIs by the user of Keycloak,
by default Keycloak as of 3.4 ships with at least
- C(docker-v2-allow-all-mapper)
- C(oidc-address-mapper)
- C(oidc-full-name-mapper)
- C(oidc-group-membership-mapper)
- C(oidc-hardcoded-claim-mapper)
- C(oidc-hardcoded-role-mapper)
- C(oidc-role-name-mapper)
- C(oidc-script-based-protocol-mapper)
- C(oidc-sha256-pairwise-sub-mapper)
- C(oidc-usermodel-attribute-mapper)
- C(oidc-usermodel-client-role-mapper)
- C(oidc-usermodel-property-mapper)
- C(oidc-usermodel-realm-role-mapper)
- C(oidc-usersessionmodel-note-mapper)
- C(saml-group-membership-mapper)
- C(saml-hardcode-attribute-mapper)
- C(saml-hardcode-role-mapper)
- C(saml-role-list-mapper)
- C(saml-role-name-mapper)
- C(saml-user-attribute-mapper)
- C(saml-user-property-mapper)
- C(saml-user-session-note-mapper)
by default Keycloak as of 3.4 ships with at least:"
- V(docker-v2-allow-all-mapper)
- V(oidc-address-mapper)
- V(oidc-full-name-mapper)
- V(oidc-group-membership-mapper)
- V(oidc-hardcoded-claim-mapper)
- V(oidc-hardcoded-role-mapper)
- V(oidc-role-name-mapper)
- V(oidc-script-based-protocol-mapper)
- V(oidc-sha256-pairwise-sub-mapper)
- V(oidc-usermodel-attribute-mapper)
- V(oidc-usermodel-client-role-mapper)
- V(oidc-usermodel-property-mapper)
- V(oidc-usermodel-realm-role-mapper)
- V(oidc-usersessionmodel-note-mapper)
- V(saml-group-membership-mapper)
- V(saml-hardcode-attribute-mapper)
- V(saml-hardcode-role-mapper)
- V(saml-role-list-mapper)
- V(saml-role-name-mapper)
- V(saml-user-attribute-mapper)
- V(saml-user-property-mapper)
- V(saml-user-session-note-mapper)
- An exhaustive list of available mappers on your installation can be obtained on
the admin console by going to Server Info -> Providers and looking under
'protocol-mapper'.
@ -431,10 +469,10 @@ options:
config:
description:
- Dict specifying the configuration options for the protocol mapper; the
contents differ depending on the value of I(protocolMapper) and are not documented
contents differ depending on the value of O(protocol_mappers[].protocolMapper) and are not documented
other than by the source of the mappers and its parent class(es). An example is given
below. It is easiest to obtain valid config values by dumping an already-existing
protocol mapper configuration through check-mode in the I(existing) field.
protocol mapper configuration through check-mode in the RV(existing) field.
type: dict
attributes:
@ -478,7 +516,7 @@ options:
saml.signature.algorithm:
description:
- Signature algorithm used to sign SAML documents. One of C(RSA_SHA256), C(RSA_SHA1), C(RSA_SHA512), or C(DSA_SHA1).
- Signature algorithm used to sign SAML documents. One of V(RSA_SHA256), V(RSA_SHA1), V(RSA_SHA512), or V(DSA_SHA1).
saml.signing.certificate:
description:
@ -496,22 +534,21 @@ options:
description:
- SAML Redirect Binding URL for the client's assertion consumer service (login responses).
saml_force_name_id_format:
description:
- For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead.
saml_name_id_format:
description:
- For SAML clients, the NameID format to use (one of C(username), C(email), C(transient), or C(persistent))
- For SAML clients, the NameID format to use (one of V(username), V(email), V(transient), or V(persistent))
saml_signature_canonicalization_method:
description:
- SAML signature canonicalization method. This is one of four values, namely
C(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE,
C(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS,
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and
C(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS.
V(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE,
V(http://www.w3.org/2001/10/xml-exc-c14n#WithComments) for EXCLUSIVE_WITH_COMMENTS,
V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315) for INCLUSIVE, and
V(http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS.
saml_single_logout_service_url_post:
description:
@ -523,12 +560,12 @@ options:
user.info.response.signature.alg:
description:
- For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of C(RS256) or C(unsigned).
- For OpenID-Connect clients, JWA algorithm for signed UserInfo-endpoint responses. One of V(RS256) or V(unsigned).
request.object.signature.alg:
description:
- For OpenID-Connect clients, JWA algorithm which the client needs to use when sending
OIDC request object. One of C(any), C(none), C(RS256).
OIDC request object. One of V(any), V(none), V(RS256).
use.jwks.url:
description:
@ -544,9 +581,21 @@ options:
- For OpenID-Connect clients, client certificate for validating JWT issued by
client and signed by its key, base64-encoded.
x509.subjectdn:
description:
- For OpenID-Connect clients, subject which will be used to authenticate the client.
type: str
version_added: 9.5.0
x509.allow.regex.pattern.comparison:
description:
- For OpenID-Connect clients, boolean specifying whether to allow C(x509.subjectdn) as regular expression.
type: bool
version_added: 9.5.0
extends_documentation_fragment:
- middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.attributes
- middleware_automation.keycloak.keycloak
- middleware_automation.keycloak.attributes
author:
- Eike Frost (@eikef)
@ -587,6 +636,22 @@ EXAMPLES = '''
delegate_to: localhost
- name: Create or update a Keycloak client (minimal example), with x509 authentication
middleware_automation.keycloak.keycloak_client:
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
realm: master
state: present
client_id: test
client_authenticator_type: client-x509
attributes:
x509.subjectdn: "CN=client"
x509.allow.regex.pattern.comparison: false
- name: Create or update a Keycloak client (with all the bells and whistles)
middleware_automation.keycloak.keycloak_client:
auth_client_id: admin-cli
@ -637,7 +702,7 @@ EXAMPLES = '''
- test01
- test02
authentication_flow_binding_overrides:
browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb
browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb
protocol_mappers:
- config:
access.token.claim: true
@ -717,11 +782,17 @@ end_state:
'''
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError
keycloak_argument_spec, get_token, KeycloakError, is_struct_included
from ansible.module_utils.basic import AnsibleModule
import copy
PROTOCOL_OPENID_CONNECT = 'openid-connect'
PROTOCOL_SAML = 'saml'
PROTOCOL_DOCKER_V2 = 'docker-v2'
CLIENT_META_DATA = ['authorizationServicesEnabled']
def normalise_cr(clientrep, remove_ids=False):
""" Re-sorts any properties where the order so that diff's is minimised, and adds default values where appropriate so that the
the change detection is more effective.
@ -737,6 +808,12 @@ def normalise_cr(clientrep, remove_ids=False):
if 'attributes' in clientrep:
clientrep['attributes'] = list(sorted(clientrep['attributes']))
if 'defaultClientScopes' in clientrep:
clientrep['defaultClientScopes'] = list(sorted(clientrep['defaultClientScopes']))
if 'optionalClientScopes' in clientrep:
clientrep['optionalClientScopes'] = list(sorted(clientrep['optionalClientScopes']))
if 'redirectUris' in clientrep:
clientrep['redirectUris'] = list(sorted(clientrep['redirectUris']))
@ -762,11 +839,70 @@ def sanitize_cr(clientrep):
if 'secret' in result:
result['secret'] = 'no_log'
if 'attributes' in result:
if 'saml.signing.private.key' in result['attributes']:
result['attributes']['saml.signing.private.key'] = 'no_log'
attributes = result['attributes']
if isinstance(attributes, dict) and 'saml.signing.private.key' in attributes:
attributes['saml.signing.private.key'] = 'no_log'
return normalise_cr(result)
def get_authentication_flow_id(flow_name, realm, kc):
""" Get the authentication flow ID based on the flow name, realm, and Keycloak client.
Args:
flow_name (str): The name of the authentication flow.
realm (str): The name of the realm.
kc (KeycloakClient): The Keycloak client instance.
Returns:
str: The ID of the authentication flow.
Raises:
KeycloakAPIException: If the authentication flow with the given name is not found in the realm.
"""
flow = kc.get_authentication_flow_by_alias(flow_name, realm)
if flow:
return flow["id"]
kc.module.fail_json(msg='Authentification flow %s not found in realm %s' % (flow_name, realm))
def flow_binding_from_dict_to_model(newClientFlowBinding, realm, kc):
""" Convert a dictionary representing client flow bindings to a model representation.
Args:
newClientFlowBinding (dict): A dictionary containing client flow bindings.
realm (str): The name of the realm.
kc (KeycloakClient): An instance of the KeycloakClient class.
Returns:
dict: A dictionary representing the model flow bindings. The dictionary has two keys:
- "browser" (str or None): The ID of the browser authentication flow binding, or None if not provided.
- "direct_grant" (str or None): The ID of the direct grant authentication flow binding, or None if not provided.
Raises:
KeycloakAPIException: If the authentication flow with the given name is not found in the realm.
"""
modelFlow = {
"browser": None,
"direct_grant": None
}
for k, v in newClientFlowBinding.items():
if not v:
continue
if k == "browser":
modelFlow["browser"] = v
elif k == "browser_name":
modelFlow["browser"] = get_authentication_flow_id(v, realm, kc)
elif k == "direct_grant":
modelFlow["direct_grant"] = v
elif k == "direct_grant_name":
modelFlow["direct_grant"] = get_authentication_flow_id(v, realm, kc)
return modelFlow
def main():
"""
Module execution
@ -780,11 +916,18 @@ def main():
consentText=dict(type='str'),
id=dict(type='str'),
name=dict(type='str'),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
authentication_flow_spec = dict(
browser=dict(type='str'),
browser_name=dict(type='str', aliases=['browserName']),
direct_grant=dict(type='str', aliases=['directGrant']),
direct_grant_name=dict(type='str', aliases=['directGrantName']),
)
meta_args = dict(
state=dict(default='present', choices=['present', 'absent']),
realm=dict(type='str', default='master'),
@ -798,7 +941,7 @@ def main():
base_url=dict(type='str', aliases=['baseUrl']),
surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']),
enabled=dict(type='bool'),
client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt'], aliases=['clientAuthenticatorType']),
client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt', 'client-x509'], aliases=['clientAuthenticatorType']),
secret=dict(type='str', no_log=True),
registration_access_token=dict(type='str', aliases=['registrationAccessToken'], no_log=True),
default_roles=dict(type='list', elements='str', aliases=['defaultRoles']),
@ -814,7 +957,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']),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
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']),
node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']),
@ -824,7 +967,13 @@ def main():
use_template_scope=dict(type='bool', aliases=['useTemplateScope']),
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']),
authentication_flow_binding_overrides=dict(type='dict', aliases=['authenticationFlowBindingOverrides']),
authentication_flow_binding_overrides=dict(
type='dict',
aliases=['authenticationFlowBindingOverrides'],
options=authentication_flow_spec,
required_one_of=[['browser', 'direct_grant', 'browser_name', 'direct_grant_name']],
mutually_exclusive=[['browser', 'browser_name'], ['direct_grant', 'direct_grant_name']],
),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']),
@ -885,7 +1034,9 @@ def main():
# Unfortunately, the ansible argument spec checker introduces variables with null values when
# they are not specified
if client_param == 'protocol_mappers':
new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value]
new_param_value = [{k: v for k, v in x.items() if v is not None} for x in new_param_value]
elif client_param == 'authentication_flow_binding_overrides':
new_param_value = flow_binding_from_dict_to_model(new_param_value, realm, kc)
changeset[camel(client_param)] = new_param_value
@ -912,6 +1063,8 @@ def main():
if 'clientId' not in desired_client:
module.fail_json(msg='client_id needs to be specified when creating a new client')
if 'protocol' not in desired_client:
desired_client['protocol'] = PROTOCOL_OPENID_CONNECT
if module._diff:
result['diff'] = dict(before='', after=sanitize_cr(desired_client))
@ -940,7 +1093,7 @@ def main():
if module._diff:
result['diff'] = dict(before=sanitize_cr(before_norm),
after=sanitize_cr(desired_norm))
result['changed'] = (before_norm != desired_norm)
result['changed'] = not is_struct_included(desired_norm, before_norm, CLIENT_META_DATA)
module.exit_json(**result)