mirror of
https://github.com/ansible-middleware/keycloak.git
synced 2025-06-03 23:49:12 -07:00
update keycloak modules
This commit is contained in:
parent
ac4511bea9
commit
c6bb815979
4 changed files with 1605 additions and 289 deletions
File diff suppressed because it is too large
Load diff
|
@ -40,8 +40,8 @@ options:
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
- State of the client
|
- State of the client
|
||||||
- On C(present), the client will be created (or updated if it exists already).
|
- On V(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(absent), the client will be removed if it exists
|
||||||
choices: ['present', 'absent']
|
choices: ['present', 'absent']
|
||||||
default: 'present'
|
default: 'present'
|
||||||
type: str
|
type: str
|
||||||
|
@ -55,7 +55,7 @@ options:
|
||||||
client_id:
|
client_id:
|
||||||
description:
|
description:
|
||||||
- Client id of client to be worked on. This is usually an alphanumeric name chosen by
|
- 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.
|
This is 'clientId' in the Keycloak REST API.
|
||||||
aliases:
|
aliases:
|
||||||
- clientId
|
- clientId
|
||||||
|
@ -63,13 +63,13 @@ options:
|
||||||
|
|
||||||
id:
|
id:
|
||||||
description:
|
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.
|
is required. If you specify both, this takes precedence.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
name:
|
name:
|
||||||
description:
|
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
|
type: str
|
||||||
|
|
||||||
description:
|
description:
|
||||||
|
@ -108,20 +108,21 @@ options:
|
||||||
|
|
||||||
client_authenticator_type:
|
client_authenticator_type:
|
||||||
description:
|
description:
|
||||||
- How do clients authenticate with the auth server? Either C(client-secret) or
|
- How do clients authenticate with the auth server? Either V(client-secret),
|
||||||
C(client-jwt) can be chosen. When using C(client-secret), the module parameter
|
V(client-jwt), or V(client-x509) can be chosen. When using V(client-secret), the module parameter
|
||||||
I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url),
|
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 I(attributes) module parameter
|
C(jwks.url), and C(jwt.credential.certificate) in the O(attributes) module parameter
|
||||||
to configure its behavior.
|
to configure its behavior. For V(client-x509) you can use the keys C(x509.allow.regex.pattern.comparison)
|
||||||
This is 'clientAuthenticatorType' in the Keycloak REST API.
|
and C(x509.subjectdn) in the O(attributes) module parameter to configure which certificate(s) to accept.
|
||||||
choices: ['client-secret', 'client-jwt']
|
- This is 'clientAuthenticatorType' in the Keycloak REST API.
|
||||||
|
choices: ['client-secret', 'client-jwt', 'client-x509']
|
||||||
aliases:
|
aliases:
|
||||||
- clientAuthenticatorType
|
- clientAuthenticatorType
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
secret:
|
secret:
|
||||||
description:
|
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
|
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
|
changing this secret, the module will not register a change currently (but the
|
||||||
changed secret will be saved).
|
changed secret will be saved).
|
||||||
|
@ -246,9 +247,11 @@ options:
|
||||||
|
|
||||||
protocol:
|
protocol:
|
||||||
description:
|
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
|
type: str
|
||||||
choices: ['openid-connect', 'saml']
|
choices: ['openid-connect', 'saml', 'docker-v2']
|
||||||
|
|
||||||
full_scope_allowed:
|
full_scope_allowed:
|
||||||
description:
|
description:
|
||||||
|
@ -286,7 +289,7 @@ options:
|
||||||
|
|
||||||
use_template_config:
|
use_template_config:
|
||||||
description:
|
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.
|
This is 'useTemplateConfig' in the Keycloak REST API.
|
||||||
aliases:
|
aliases:
|
||||||
- useTemplateConfig
|
- useTemplateConfig
|
||||||
|
@ -294,7 +297,7 @@ options:
|
||||||
|
|
||||||
use_template_scope:
|
use_template_scope:
|
||||||
description:
|
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.
|
This is 'useTemplateScope' in the Keycloak REST API.
|
||||||
aliases:
|
aliases:
|
||||||
- useTemplateScope
|
- useTemplateScope
|
||||||
|
@ -302,7 +305,7 @@ options:
|
||||||
|
|
||||||
use_template_mappers:
|
use_template_mappers:
|
||||||
description:
|
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.
|
This is 'useTemplateMappers' in the Keycloak REST API.
|
||||||
aliases:
|
aliases:
|
||||||
- useTemplateMappers
|
- useTemplateMappers
|
||||||
|
@ -338,6 +341,42 @@ options:
|
||||||
description:
|
description:
|
||||||
- Override realm authentication flow bindings.
|
- Override realm authentication flow bindings.
|
||||||
type: dict
|
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:
|
aliases:
|
||||||
- authenticationFlowBindingOverrides
|
- authenticationFlowBindingOverrides
|
||||||
version_added: 3.4.0
|
version_added: 3.4.0
|
||||||
|
@ -391,38 +430,37 @@ options:
|
||||||
|
|
||||||
protocol:
|
protocol:
|
||||||
description:
|
description:
|
||||||
- This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper.
|
- This specifies for which protocol this protocol mapper is active.
|
||||||
is active.
|
choices: ['openid-connect', 'saml', 'docker-v2']
|
||||||
choices: ['openid-connect', 'saml']
|
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
protocolMapper:
|
protocolMapper:
|
||||||
description:
|
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,
|
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
|
by default Keycloak as of 3.4 ships with at least:"
|
||||||
- C(docker-v2-allow-all-mapper)
|
- V(docker-v2-allow-all-mapper)
|
||||||
- C(oidc-address-mapper)
|
- V(oidc-address-mapper)
|
||||||
- C(oidc-full-name-mapper)
|
- V(oidc-full-name-mapper)
|
||||||
- C(oidc-group-membership-mapper)
|
- V(oidc-group-membership-mapper)
|
||||||
- C(oidc-hardcoded-claim-mapper)
|
- V(oidc-hardcoded-claim-mapper)
|
||||||
- C(oidc-hardcoded-role-mapper)
|
- V(oidc-hardcoded-role-mapper)
|
||||||
- C(oidc-role-name-mapper)
|
- V(oidc-role-name-mapper)
|
||||||
- C(oidc-script-based-protocol-mapper)
|
- V(oidc-script-based-protocol-mapper)
|
||||||
- C(oidc-sha256-pairwise-sub-mapper)
|
- V(oidc-sha256-pairwise-sub-mapper)
|
||||||
- C(oidc-usermodel-attribute-mapper)
|
- V(oidc-usermodel-attribute-mapper)
|
||||||
- C(oidc-usermodel-client-role-mapper)
|
- V(oidc-usermodel-client-role-mapper)
|
||||||
- C(oidc-usermodel-property-mapper)
|
- V(oidc-usermodel-property-mapper)
|
||||||
- C(oidc-usermodel-realm-role-mapper)
|
- V(oidc-usermodel-realm-role-mapper)
|
||||||
- C(oidc-usersessionmodel-note-mapper)
|
- V(oidc-usersessionmodel-note-mapper)
|
||||||
- C(saml-group-membership-mapper)
|
- V(saml-group-membership-mapper)
|
||||||
- C(saml-hardcode-attribute-mapper)
|
- V(saml-hardcode-attribute-mapper)
|
||||||
- C(saml-hardcode-role-mapper)
|
- V(saml-hardcode-role-mapper)
|
||||||
- C(saml-role-list-mapper)
|
- V(saml-role-list-mapper)
|
||||||
- C(saml-role-name-mapper)
|
- V(saml-role-name-mapper)
|
||||||
- C(saml-user-attribute-mapper)
|
- V(saml-user-attribute-mapper)
|
||||||
- C(saml-user-property-mapper)
|
- V(saml-user-property-mapper)
|
||||||
- C(saml-user-session-note-mapper)
|
- V(saml-user-session-note-mapper)
|
||||||
- An exhaustive list of available mappers on your installation can be obtained on
|
- 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
|
the admin console by going to Server Info -> Providers and looking under
|
||||||
'protocol-mapper'.
|
'protocol-mapper'.
|
||||||
|
@ -431,10 +469,10 @@ options:
|
||||||
config:
|
config:
|
||||||
description:
|
description:
|
||||||
- Dict specifying the configuration options for the protocol mapper; the
|
- 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
|
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
|
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
|
type: dict
|
||||||
|
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -478,7 +516,7 @@ options:
|
||||||
|
|
||||||
saml.signature.algorithm:
|
saml.signature.algorithm:
|
||||||
description:
|
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:
|
saml.signing.certificate:
|
||||||
description:
|
description:
|
||||||
|
@ -496,22 +534,21 @@ options:
|
||||||
description:
|
description:
|
||||||
- SAML Redirect Binding URL for the client's assertion consumer service (login responses).
|
- SAML Redirect Binding URL for the client's assertion consumer service (login responses).
|
||||||
|
|
||||||
|
|
||||||
saml_force_name_id_format:
|
saml_force_name_id_format:
|
||||||
description:
|
description:
|
||||||
- For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead.
|
- For SAML clients, Boolean specifying whether to ignore requested NameID subject format and using the configured one instead.
|
||||||
|
|
||||||
saml_name_id_format:
|
saml_name_id_format:
|
||||||
description:
|
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:
|
saml_signature_canonicalization_method:
|
||||||
description:
|
description:
|
||||||
- SAML signature canonicalization method. This is one of four values, namely
|
- SAML signature canonicalization method. This is one of four values, namely
|
||||||
C(http://www.w3.org/2001/10/xml-exc-c14n#) for EXCLUSIVE,
|
V(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,
|
V(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
|
V(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/TR/2001/REC-xml-c14n-20010315#WithComments) for INCLUSIVE_WITH_COMMENTS.
|
||||||
|
|
||||||
saml_single_logout_service_url_post:
|
saml_single_logout_service_url_post:
|
||||||
description:
|
description:
|
||||||
|
@ -523,12 +560,12 @@ options:
|
||||||
|
|
||||||
user.info.response.signature.alg:
|
user.info.response.signature.alg:
|
||||||
description:
|
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:
|
request.object.signature.alg:
|
||||||
description:
|
description:
|
||||||
- For OpenID-Connect clients, JWA algorithm which the client needs to use when sending
|
- 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:
|
use.jwks.url:
|
||||||
description:
|
description:
|
||||||
|
@ -544,9 +581,21 @@ options:
|
||||||
- For OpenID-Connect clients, client certificate for validating JWT issued by
|
- For OpenID-Connect clients, client certificate for validating JWT issued by
|
||||||
client and signed by its key, base64-encoded.
|
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:
|
extends_documentation_fragment:
|
||||||
- middleware_automation.keycloak.keycloak
|
- middleware_automation.keycloak.keycloak
|
||||||
- middleware_automation.keycloak.attributes
|
- middleware_automation.keycloak.attributes
|
||||||
|
|
||||||
author:
|
author:
|
||||||
- Eike Frost (@eikef)
|
- Eike Frost (@eikef)
|
||||||
|
@ -587,6 +636,22 @@ EXAMPLES = '''
|
||||||
delegate_to: localhost
|
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)
|
- name: Create or update a Keycloak client (with all the bells and whistles)
|
||||||
middleware_automation.keycloak.keycloak_client:
|
middleware_automation.keycloak.keycloak_client:
|
||||||
auth_client_id: admin-cli
|
auth_client_id: admin-cli
|
||||||
|
@ -637,7 +702,7 @@ EXAMPLES = '''
|
||||||
- test01
|
- test01
|
||||||
- test02
|
- test02
|
||||||
authentication_flow_binding_overrides:
|
authentication_flow_binding_overrides:
|
||||||
browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb
|
browser: 4c90336b-bf1d-4b87-916d-3677ba4e5fbb
|
||||||
protocol_mappers:
|
protocol_mappers:
|
||||||
- config:
|
- config:
|
||||||
access.token.claim: true
|
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, \
|
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
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
import copy
|
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):
|
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
|
""" 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.
|
the change detection is more effective.
|
||||||
|
@ -737,6 +808,12 @@ def normalise_cr(clientrep, remove_ids=False):
|
||||||
if 'attributes' in clientrep:
|
if 'attributes' in clientrep:
|
||||||
clientrep['attributes'] = list(sorted(clientrep['attributes']))
|
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:
|
if 'redirectUris' in clientrep:
|
||||||
clientrep['redirectUris'] = list(sorted(clientrep['redirectUris']))
|
clientrep['redirectUris'] = list(sorted(clientrep['redirectUris']))
|
||||||
|
|
||||||
|
@ -762,11 +839,70 @@ def sanitize_cr(clientrep):
|
||||||
if 'secret' in result:
|
if 'secret' in result:
|
||||||
result['secret'] = 'no_log'
|
result['secret'] = 'no_log'
|
||||||
if 'attributes' in result:
|
if 'attributes' in result:
|
||||||
if 'saml.signing.private.key' in result['attributes']:
|
attributes = result['attributes']
|
||||||
result['attributes']['saml.signing.private.key'] = 'no_log'
|
if isinstance(attributes, dict) and 'saml.signing.private.key' in attributes:
|
||||||
|
attributes['saml.signing.private.key'] = 'no_log'
|
||||||
return normalise_cr(result)
|
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():
|
def main():
|
||||||
"""
|
"""
|
||||||
Module execution
|
Module execution
|
||||||
|
@ -780,11 +916,18 @@ def main():
|
||||||
consentText=dict(type='str'),
|
consentText=dict(type='str'),
|
||||||
id=dict(type='str'),
|
id=dict(type='str'),
|
||||||
name=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'),
|
protocolMapper=dict(type='str'),
|
||||||
config=dict(type='dict'),
|
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(
|
meta_args = dict(
|
||||||
state=dict(default='present', choices=['present', 'absent']),
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
realm=dict(type='str', default='master'),
|
realm=dict(type='str', default='master'),
|
||||||
|
@ -798,7 +941,7 @@ def main():
|
||||||
base_url=dict(type='str', aliases=['baseUrl']),
|
base_url=dict(type='str', aliases=['baseUrl']),
|
||||||
surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']),
|
surrogate_auth_required=dict(type='bool', aliases=['surrogateAuthRequired']),
|
||||||
enabled=dict(type='bool'),
|
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),
|
secret=dict(type='str', no_log=True),
|
||||||
registration_access_token=dict(type='str', aliases=['registrationAccessToken'], no_log=True),
|
registration_access_token=dict(type='str', aliases=['registrationAccessToken'], no_log=True),
|
||||||
default_roles=dict(type='list', elements='str', aliases=['defaultRoles']),
|
default_roles=dict(type='list', elements='str', aliases=['defaultRoles']),
|
||||||
|
@ -814,7 +957,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']),
|
||||||
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'),
|
attributes=dict(type='dict'),
|
||||||
full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']),
|
full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']),
|
||||||
node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']),
|
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_scope=dict(type='bool', aliases=['useTemplateScope']),
|
||||||
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
|
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
|
||||||
always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']),
|
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']),
|
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
|
||||||
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
|
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
|
||||||
default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']),
|
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
|
# 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':
|
||||||
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
|
changeset[camel(client_param)] = new_param_value
|
||||||
|
|
||||||
|
@ -912,6 +1063,8 @@ def main():
|
||||||
|
|
||||||
if 'clientId' not in desired_client:
|
if 'clientId' not in desired_client:
|
||||||
module.fail_json(msg='client_id needs to be specified when creating a new 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:
|
if module._diff:
|
||||||
result['diff'] = dict(before='', after=sanitize_cr(desired_client))
|
result['diff'] = dict(before='', after=sanitize_cr(desired_client))
|
||||||
|
@ -940,7 +1093,7 @@ def main():
|
||||||
if module._diff:
|
if module._diff:
|
||||||
result['diff'] = dict(before=sanitize_cr(before_norm),
|
result['diff'] = dict(before=sanitize_cr(before_norm),
|
||||||
after=sanitize_cr(desired_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)
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,8 @@ options:
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
- State of the role.
|
- State of the role.
|
||||||
- On C(present), the role will be created if it does not yet exist, or updated with the parameters you provide.
|
- On V(present), the role will be created if it does not yet exist, or updated with the parameters you provide.
|
||||||
- On C(absent), the role will be removed if it exists.
|
- On V(absent), the role will be removed if it exists.
|
||||||
default: 'present'
|
default: 'present'
|
||||||
type: str
|
type: str
|
||||||
choices:
|
choices:
|
||||||
|
@ -77,6 +77,42 @@ options:
|
||||||
description:
|
description:
|
||||||
- A dict of key/value pairs to set as custom attributes for the role.
|
- A dict of key/value pairs to set as custom attributes for the role.
|
||||||
- Values may be single values (e.g. a string) or a list of strings.
|
- Values may be single values (e.g. a string) or a list of strings.
|
||||||
|
composite:
|
||||||
|
description:
|
||||||
|
- If V(true), the role is a composition of other realm and/or client role.
|
||||||
|
default: false
|
||||||
|
type: bool
|
||||||
|
version_added: 7.1.0
|
||||||
|
composites:
|
||||||
|
description:
|
||||||
|
- List of roles to include to the composite realm role.
|
||||||
|
- If the composite role is a client role, the C(clientId) (not ID of the client) must be specified.
|
||||||
|
default: []
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
version_added: 7.1.0
|
||||||
|
suboptions:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the role. This can be the name of a REALM role or a client role.
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
client_id:
|
||||||
|
description:
|
||||||
|
- Client ID if the role is a client role. Do not include this option for a REALM role.
|
||||||
|
- Use the client ID you can see in the Keycloak console, not the technical ID of the client.
|
||||||
|
type: str
|
||||||
|
required: false
|
||||||
|
aliases:
|
||||||
|
- clientId
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Create the composite if present, remove it if absent.
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
- present
|
||||||
|
- absent
|
||||||
|
default: present
|
||||||
|
|
||||||
extends_documentation_fragment:
|
extends_documentation_fragment:
|
||||||
- middleware_automation.keycloak.keycloak
|
- middleware_automation.keycloak.keycloak
|
||||||
|
@ -142,14 +178,14 @@ EXAMPLES = '''
|
||||||
auth_password: PASSWORD
|
auth_password: PASSWORD
|
||||||
name: my-new-role
|
name: my-new-role
|
||||||
attributes:
|
attributes:
|
||||||
attrib1: value1
|
attrib1: value1
|
||||||
attrib2: value2
|
attrib2: value2
|
||||||
attrib3:
|
attrib3:
|
||||||
- with
|
- with
|
||||||
- numerous
|
- numerous
|
||||||
- individual
|
- individual
|
||||||
- list
|
- list
|
||||||
- items
|
- items
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -198,8 +234,9 @@ end_state:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible_collections.middleware_automation.keycloak.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
|
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
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -210,6 +247,12 @@ def main():
|
||||||
"""
|
"""
|
||||||
argument_spec = keycloak_argument_spec()
|
argument_spec = keycloak_argument_spec()
|
||||||
|
|
||||||
|
composites_spec = dict(
|
||||||
|
name=dict(type='str', required=True),
|
||||||
|
client_id=dict(type='str', aliases=['clientId'], required=False),
|
||||||
|
state=dict(type='str', default='present', choices=['present', 'absent'])
|
||||||
|
)
|
||||||
|
|
||||||
meta_args = dict(
|
meta_args = dict(
|
||||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||||
name=dict(type='str', required=True),
|
name=dict(type='str', required=True),
|
||||||
|
@ -217,6 +260,8 @@ def main():
|
||||||
realm=dict(type='str', default='master'),
|
realm=dict(type='str', default='master'),
|
||||||
client_id=dict(type='str'),
|
client_id=dict(type='str'),
|
||||||
attributes=dict(type='dict'),
|
attributes=dict(type='dict'),
|
||||||
|
composites=dict(type='list', default=[], options=composites_spec, elements='dict'),
|
||||||
|
composite=dict(type='bool', default=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
argument_spec.update(meta_args)
|
argument_spec.update(meta_args)
|
||||||
|
@ -250,7 +295,7 @@ def main():
|
||||||
|
|
||||||
# Filter and map the parameters names that apply to the role
|
# Filter and map the parameters names that apply to the role
|
||||||
role_params = [x for x in module.params
|
role_params = [x for x in module.params
|
||||||
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id', 'composites'] and
|
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id'] and
|
||||||
module.params.get(x) is not None]
|
module.params.get(x) is not None]
|
||||||
|
|
||||||
# See if it already exists in Keycloak
|
# See if it already exists in Keycloak
|
||||||
|
@ -269,10 +314,10 @@ def main():
|
||||||
new_param_value = module.params.get(param)
|
new_param_value = module.params.get(param)
|
||||||
old_value = before_role[param] if param in before_role else None
|
old_value = before_role[param] if param in before_role else None
|
||||||
if new_param_value != old_value:
|
if new_param_value != old_value:
|
||||||
changeset[camel(param)] = new_param_value
|
changeset[camel(param)] = copy.deepcopy(new_param_value)
|
||||||
|
|
||||||
# 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_role = before_role.copy()
|
desired_role = copy.deepcopy(before_role)
|
||||||
desired_role.update(changeset)
|
desired_role.update(changeset)
|
||||||
|
|
||||||
result['proposed'] = changeset
|
result['proposed'] = changeset
|
||||||
|
@ -309,6 +354,9 @@ def main():
|
||||||
kc.create_client_role(desired_role, clientid, realm)
|
kc.create_client_role(desired_role, clientid, realm)
|
||||||
after_role = kc.get_client_role(name, clientid, realm)
|
after_role = kc.get_client_role(name, clientid, realm)
|
||||||
|
|
||||||
|
if after_role['composite']:
|
||||||
|
after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
|
||||||
|
|
||||||
result['end_state'] = after_role
|
result['end_state'] = after_role
|
||||||
|
|
||||||
result['msg'] = 'Role {name} has been created'.format(name=name)
|
result['msg'] = 'Role {name} has been created'.format(name=name)
|
||||||
|
@ -316,10 +364,25 @@ def main():
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
|
compare_exclude = []
|
||||||
|
if 'composites' in desired_role and isinstance(desired_role['composites'], list) and len(desired_role['composites']) > 0:
|
||||||
|
composites = kc.get_role_composites(rolerep=before_role, clientid=clientid, realm=realm)
|
||||||
|
before_role['composites'] = []
|
||||||
|
for composite in composites:
|
||||||
|
before_composite = {}
|
||||||
|
if composite['clientRole']:
|
||||||
|
composite_client = kc.get_client_by_id(id=composite['containerId'], realm=realm)
|
||||||
|
before_composite['client_id'] = composite_client['clientId']
|
||||||
|
else:
|
||||||
|
before_composite['client_id'] = None
|
||||||
|
before_composite['name'] = composite['name']
|
||||||
|
before_composite['state'] = 'present'
|
||||||
|
before_role['composites'].append(before_composite)
|
||||||
|
else:
|
||||||
|
compare_exclude.append('composites')
|
||||||
# Process an update
|
# Process an update
|
||||||
|
|
||||||
# no changes
|
# no changes
|
||||||
if desired_role == before_role:
|
if is_struct_included(desired_role, before_role, exclude=compare_exclude):
|
||||||
result['changed'] = False
|
result['changed'] = False
|
||||||
result['end_state'] = desired_role
|
result['end_state'] = desired_role
|
||||||
result['msg'] = "No changes required to role {name}.".format(name=name)
|
result['msg'] = "No changes required to role {name}.".format(name=name)
|
||||||
|
@ -341,6 +404,8 @@ def main():
|
||||||
else:
|
else:
|
||||||
kc.update_client_role(desired_role, clientid, realm)
|
kc.update_client_role(desired_role, clientid, realm)
|
||||||
after_role = kc.get_client_role(name, clientid, realm)
|
after_role = kc.get_client_role(name, clientid, realm)
|
||||||
|
if after_role['composite']:
|
||||||
|
after_role['composites'] = kc.get_role_composites(rolerep=after_role, clientid=clientid, realm=realm)
|
||||||
|
|
||||||
result['end_state'] = after_role
|
result['end_state'] = after_role
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,9 @@ options:
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
- State of the user federation.
|
- State of the user federation.
|
||||||
- On C(present), the user federation will be created if it does not yet exist, or updated with
|
- On V(present), the user federation will be created if it does not yet exist, or updated with
|
||||||
the parameters you provide.
|
the parameters you provide.
|
||||||
- On C(absent), the user federation will be removed if it exists.
|
- On V(absent), the user federation will be removed if it exists.
|
||||||
default: 'present'
|
default: 'present'
|
||||||
type: str
|
type: str
|
||||||
choices:
|
choices:
|
||||||
|
@ -54,7 +54,7 @@ options:
|
||||||
id:
|
id:
|
||||||
description:
|
description:
|
||||||
- The unique ID for this user federation. If left empty, the user federation will be searched
|
- The unique ID for this user federation. If left empty, the user federation will be searched
|
||||||
by its I(name).
|
by its O(name).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
name:
|
name:
|
||||||
|
@ -64,18 +64,15 @@ options:
|
||||||
|
|
||||||
provider_id:
|
provider_id:
|
||||||
description:
|
description:
|
||||||
- Provider for this user federation.
|
- Provider for this user federation. Built-in providers are V(ldap), V(kerberos), and V(sssd).
|
||||||
|
Custom user storage providers can also be used.
|
||||||
aliases:
|
aliases:
|
||||||
- providerId
|
- providerId
|
||||||
type: str
|
type: str
|
||||||
choices:
|
|
||||||
- ldap
|
|
||||||
- kerberos
|
|
||||||
- sssd
|
|
||||||
|
|
||||||
provider_type:
|
provider_type:
|
||||||
description:
|
description:
|
||||||
- Component type for user federation (only supported value is C(org.keycloak.storage.UserStorageProvider)).
|
- Component type for user federation (only supported value is V(org.keycloak.storage.UserStorageProvider)).
|
||||||
aliases:
|
aliases:
|
||||||
- providerType
|
- providerType
|
||||||
default: org.keycloak.storage.UserStorageProvider
|
default: org.keycloak.storage.UserStorageProvider
|
||||||
|
@ -88,13 +85,37 @@ options:
|
||||||
- parentId
|
- parentId
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
|
remove_unspecified_mappers:
|
||||||
|
description:
|
||||||
|
- Remove mappers that are not specified in the configuration for this federation.
|
||||||
|
- Set to V(false) to keep mappers that are not listed in O(mappers).
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
||||||
|
bind_credential_update_mode:
|
||||||
|
description:
|
||||||
|
- The value of the config parameter O(config.bindCredential) is redacted in the Keycloak responses.
|
||||||
|
Comparing the redacted value with the desired value always evaluates to not equal. This means
|
||||||
|
the before and desired states are never equal if the parameter is set.
|
||||||
|
- Set to V(always) to include O(config.bindCredential) in the comparison of before and desired state.
|
||||||
|
Because of the redacted value returned by Keycloak the module will always detect a change
|
||||||
|
and make an update if a O(config.bindCredential) value is set.
|
||||||
|
- Set to V(only_indirect) to exclude O(config.bindCredential) when comparing the before state with the
|
||||||
|
desired state. The value of O(config.bindCredential) will only be updated if there are other changes
|
||||||
|
to the user federation that require an update.
|
||||||
|
type: str
|
||||||
|
default: always
|
||||||
|
choices:
|
||||||
|
- always
|
||||||
|
- only_indirect
|
||||||
|
|
||||||
config:
|
config:
|
||||||
description:
|
description:
|
||||||
- Dict specifying the configuration options for the provider; the contents differ depending on
|
- Dict specifying the configuration options for the provider; the contents differ depending on
|
||||||
the value of I(provider_id). Examples are given below for C(ldap), C(kerberos) and C(sssd).
|
the value of O(provider_id). Examples are given below for V(ldap), V(kerberos) and V(sssd).
|
||||||
It is easiest to obtain valid config values by dumping an already-existing user federation
|
It is easiest to obtain valid config values by dumping an already-existing user federation
|
||||||
configuration through check-mode in the I(existing) field.
|
configuration through check-mode in the RV(existing) field.
|
||||||
- The value C(sssd) has been supported since middleware_automation.keycloak 1.0.0.
|
- The value V(sssd) has been supported since middleware_automation.keycloak 2.0.0.
|
||||||
type: dict
|
type: dict
|
||||||
suboptions:
|
suboptions:
|
||||||
enabled:
|
enabled:
|
||||||
|
@ -111,15 +132,15 @@ options:
|
||||||
|
|
||||||
importEnabled:
|
importEnabled:
|
||||||
description:
|
description:
|
||||||
- If C(true), LDAP users will be imported into Keycloak DB and synced by the configured
|
- If V(true), LDAP users will be imported into Keycloak DB and synced by the configured
|
||||||
sync policies.
|
sync policies.
|
||||||
default: true
|
default: true
|
||||||
type: bool
|
type: bool
|
||||||
|
|
||||||
editMode:
|
editMode:
|
||||||
description:
|
description:
|
||||||
- C(READ_ONLY) is a read-only LDAP store. C(WRITABLE) means data will be synced back to LDAP
|
- V(READ_ONLY) is a read-only LDAP store. V(WRITABLE) means data will be synced back to LDAP
|
||||||
on demand. C(UNSYNCED) means user data will be imported, but not synced back to LDAP.
|
on demand. V(UNSYNCED) means user data will be imported, but not synced back to LDAP.
|
||||||
type: str
|
type: str
|
||||||
choices:
|
choices:
|
||||||
- READ_ONLY
|
- READ_ONLY
|
||||||
|
@ -136,13 +157,13 @@ options:
|
||||||
vendor:
|
vendor:
|
||||||
description:
|
description:
|
||||||
- LDAP vendor (provider).
|
- LDAP vendor (provider).
|
||||||
- Use short name. For instance, write C(rhds) for "Red Hat Directory Server".
|
- Use short name. For instance, write V(rhds) for "Red Hat Directory Server".
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
usernameLDAPAttribute:
|
usernameLDAPAttribute:
|
||||||
description:
|
description:
|
||||||
- Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server
|
- Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server
|
||||||
vendors it can be C(uid). For Active directory it can be C(sAMAccountName) or C(cn).
|
vendors it can be V(uid). For Active directory it can be V(sAMAccountName) or V(cn).
|
||||||
The attribute should be filled for all LDAP user records you want to import from
|
The attribute should be filled for all LDAP user records you want to import from
|
||||||
LDAP to Keycloak.
|
LDAP to Keycloak.
|
||||||
type: str
|
type: str
|
||||||
|
@ -151,15 +172,15 @@ options:
|
||||||
description:
|
description:
|
||||||
- Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN.
|
- Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN.
|
||||||
Usually it's the same as Username LDAP attribute, however it is not required. For
|
Usually it's the same as Username LDAP attribute, however it is not required. For
|
||||||
example for Active directory, it is common to use C(cn) as RDN attribute when
|
example for Active directory, it is common to use V(cn) as RDN attribute when
|
||||||
username attribute might be C(sAMAccountName).
|
username attribute might be V(sAMAccountName).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
uuidLDAPAttribute:
|
uuidLDAPAttribute:
|
||||||
description:
|
description:
|
||||||
- Name of LDAP attribute, which is used as unique object identifier (UUID) for objects
|
- Name of LDAP attribute, which is used as unique object identifier (UUID) for objects
|
||||||
in LDAP. For many LDAP server vendors, it is C(entryUUID); however some are different.
|
in LDAP. For many LDAP server vendors, it is V(entryUUID); however some are different.
|
||||||
For example for Active directory it should be C(objectGUID). If your LDAP server does
|
For example for Active directory it should be V(objectGUID). If your LDAP server does
|
||||||
not support the notion of UUID, you can use any other attribute that is supposed to
|
not support the notion of UUID, you can use any other attribute that is supposed to
|
||||||
be unique among LDAP users in tree.
|
be unique among LDAP users in tree.
|
||||||
type: str
|
type: str
|
||||||
|
@ -167,7 +188,7 @@ options:
|
||||||
userObjectClasses:
|
userObjectClasses:
|
||||||
description:
|
description:
|
||||||
- All values of LDAP objectClass attribute for users in LDAP divided by comma.
|
- All values of LDAP objectClass attribute for users in LDAP divided by comma.
|
||||||
For example C(inetOrgPerson, organizationalPerson). Newly created Keycloak users
|
For example V(inetOrgPerson, organizationalPerson). Newly created Keycloak users
|
||||||
will be written to LDAP with all those object classes and existing LDAP user records
|
will be written to LDAP with all those object classes and existing LDAP user records
|
||||||
are found just if they contain all those object classes.
|
are found just if they contain all those object classes.
|
||||||
type: str
|
type: str
|
||||||
|
@ -251,8 +272,8 @@ options:
|
||||||
useTruststoreSpi:
|
useTruststoreSpi:
|
||||||
description:
|
description:
|
||||||
- Specifies whether LDAP connection will use the truststore SPI with the truststore
|
- Specifies whether LDAP connection will use the truststore SPI with the truststore
|
||||||
configured in standalone.xml/domain.xml. C(Always) means that it will always use it.
|
configured in standalone.xml/domain.xml. V(always) means that it will always use it.
|
||||||
C(Never) means that it will not use it. C(Only for ldaps) means that it will use if
|
V(never) means that it will not use it. V(ldapsOnly) means that it will use if
|
||||||
your connection URL use ldaps. Note even if standalone.xml/domain.xml is not
|
your connection URL use ldaps. Note even if standalone.xml/domain.xml is not
|
||||||
configured, the default Java cacerts or certificate specified by
|
configured, the default Java cacerts or certificate specified by
|
||||||
C(javax.net.ssl.trustStore) property will be used.
|
C(javax.net.ssl.trustStore) property will be used.
|
||||||
|
@ -297,7 +318,7 @@ options:
|
||||||
connectionPoolingDebug:
|
connectionPoolingDebug:
|
||||||
description:
|
description:
|
||||||
- A string that indicates the level of debug output to produce. Example valid values are
|
- A string that indicates the level of debug output to produce. Example valid values are
|
||||||
C(fine) (trace connection creation and removal) and C(all) (all debugging information).
|
V(fine) (trace connection creation and removal) and V(all) (all debugging information).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
connectionPoolingInitSize:
|
connectionPoolingInitSize:
|
||||||
|
@ -321,7 +342,7 @@ options:
|
||||||
connectionPoolingProtocol:
|
connectionPoolingProtocol:
|
||||||
description:
|
description:
|
||||||
- A list of space-separated protocol types of connections that may be pooled.
|
- A list of space-separated protocol types of connections that may be pooled.
|
||||||
Valid types are C(plain) and C(ssl).
|
Valid types are V(plain) and V(ssl).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
connectionPoolingTimeout:
|
connectionPoolingTimeout:
|
||||||
|
@ -342,17 +363,26 @@ options:
|
||||||
- Name of kerberos realm.
|
- Name of kerberos realm.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
|
krbPrincipalAttribute:
|
||||||
|
description:
|
||||||
|
- Name of the LDAP attribute, which refers to Kerberos principal.
|
||||||
|
This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak.
|
||||||
|
When this is empty, the LDAP user will be looked based on LDAP username corresponding
|
||||||
|
to the first part of his Kerberos principal. For instance, for principal C(john@KEYCLOAK.ORG),
|
||||||
|
it will assume that LDAP username is V(john).
|
||||||
|
type: str
|
||||||
|
|
||||||
serverPrincipal:
|
serverPrincipal:
|
||||||
description:
|
description:
|
||||||
- Full name of server principal for HTTP service including server and domain name. For
|
- Full name of server principal for HTTP service including server and domain name. For
|
||||||
example C(HTTP/host.foo.org@FOO.ORG). Use C(*) to accept any service principal in the
|
example V(HTTP/host.foo.org@FOO.ORG). Use V(*) to accept any service principal in the
|
||||||
KeyTab file.
|
KeyTab file.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
keyTab:
|
keyTab:
|
||||||
description:
|
description:
|
||||||
- Location of Kerberos KeyTab file containing the credentials of server principal. For
|
- Location of Kerberos KeyTab file containing the credentials of server principal. For
|
||||||
example C(/etc/krb5.keytab).
|
example V(/etc/krb5.keytab).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
|
@ -427,6 +457,16 @@ options:
|
||||||
- Max lifespan of cache entry in milliseconds.
|
- Max lifespan of cache entry in milliseconds.
|
||||||
type: int
|
type: int
|
||||||
|
|
||||||
|
referral:
|
||||||
|
description:
|
||||||
|
- Specifies if LDAP referrals should be followed or ignored. Please note that enabling
|
||||||
|
referrals can slow down authentication as it allows the LDAP server to decide which other
|
||||||
|
LDAP servers to use. This could potentially include untrusted servers.
|
||||||
|
type: str
|
||||||
|
choices:
|
||||||
|
- ignore
|
||||||
|
- follow
|
||||||
|
|
||||||
mappers:
|
mappers:
|
||||||
description:
|
description:
|
||||||
- A list of dicts defining mappers associated with this Identity Provider.
|
- A list of dicts defining mappers associated with this Identity Provider.
|
||||||
|
@ -451,7 +491,7 @@ options:
|
||||||
|
|
||||||
providerId:
|
providerId:
|
||||||
description:
|
description:
|
||||||
- The mapper type for this mapper (for instance C(user-attribute-ldap-mapper)).
|
- The mapper type for this mapper (for instance V(user-attribute-ldap-mapper)).
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
providerType:
|
providerType:
|
||||||
|
@ -534,14 +574,14 @@ EXAMPLES = '''
|
||||||
provider_id: kerberos
|
provider_id: kerberos
|
||||||
provider_type: org.keycloak.storage.UserStorageProvider
|
provider_type: org.keycloak.storage.UserStorageProvider
|
||||||
config:
|
config:
|
||||||
priority: 0
|
priority: 0
|
||||||
enabled: true
|
enabled: true
|
||||||
cachePolicy: DEFAULT
|
cachePolicy: DEFAULT
|
||||||
kerberosRealm: EXAMPLE.COM
|
kerberosRealm: EXAMPLE.COM
|
||||||
serverPrincipal: HTTP/host.example.com@EXAMPLE.COM
|
serverPrincipal: HTTP/host.example.com@EXAMPLE.COM
|
||||||
keyTab: keytab
|
keyTab: keytab
|
||||||
allowPasswordAuthentication: false
|
allowPasswordAuthentication: false
|
||||||
updateProfileFirstLogin: false
|
updateProfileFirstLogin: false
|
||||||
|
|
||||||
- name: Create sssd user federation
|
- name: Create sssd user federation
|
||||||
middleware_automation.keycloak.keycloak_user_federation:
|
middleware_automation.keycloak.keycloak_user_federation:
|
||||||
|
@ -704,16 +744,27 @@ from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_kc_comp(comp):
|
||||||
|
if 'config' in comp:
|
||||||
|
# kc completely removes the parameter `krbPrincipalAttribute` if it is set to `''`; the unset kc parameter is equivalent to `''`;
|
||||||
|
# to make change detection and diff more accurate we set it again in the kc responses
|
||||||
|
if 'krbPrincipalAttribute' not in comp['config']:
|
||||||
|
comp['config']['krbPrincipalAttribute'] = ['']
|
||||||
|
|
||||||
|
# kc stores a timestamp of the last sync in `lastSync` to time the periodic sync, it is removed to minimize diff/changes
|
||||||
|
comp['config'].pop('lastSync', None)
|
||||||
|
|
||||||
|
|
||||||
def sanitize(comp):
|
def sanitize(comp):
|
||||||
compcopy = deepcopy(comp)
|
compcopy = deepcopy(comp)
|
||||||
if 'config' in compcopy:
|
if 'config' in compcopy:
|
||||||
compcopy['config'] = dict((k, v[0]) for k, v in compcopy['config'].items())
|
compcopy['config'] = {k: v[0] for k, v in compcopy['config'].items()}
|
||||||
if 'bindCredential' in compcopy['config']:
|
if 'bindCredential' in compcopy['config']:
|
||||||
compcopy['config']['bindCredential'] = '**********'
|
compcopy['config']['bindCredential'] = '**********'
|
||||||
if 'mappers' in compcopy:
|
if 'mappers' in compcopy:
|
||||||
for mapper in compcopy['mappers']:
|
for mapper in compcopy['mappers']:
|
||||||
if 'config' in mapper:
|
if 'config' in mapper:
|
||||||
mapper['config'] = dict((k, v[0]) for k, v in mapper['config'].items())
|
mapper['config'] = {k: v[0] for k, v in mapper['config'].items()}
|
||||||
return compcopy
|
return compcopy
|
||||||
|
|
||||||
|
|
||||||
|
@ -760,8 +811,10 @@ def main():
|
||||||
priority=dict(type='int', default=0),
|
priority=dict(type='int', default=0),
|
||||||
rdnLDAPAttribute=dict(type='str'),
|
rdnLDAPAttribute=dict(type='str'),
|
||||||
readTimeout=dict(type='int'),
|
readTimeout=dict(type='int'),
|
||||||
|
referral=dict(type='str', choices=['ignore', 'follow']),
|
||||||
searchScope=dict(type='str', choices=['1', '2'], default='1'),
|
searchScope=dict(type='str', choices=['1', '2'], default='1'),
|
||||||
serverPrincipal=dict(type='str'),
|
serverPrincipal=dict(type='str'),
|
||||||
|
krbPrincipalAttribute=dict(type='str'),
|
||||||
startTls=dict(type='bool', default=False),
|
startTls=dict(type='bool', default=False),
|
||||||
syncRegistrations=dict(type='bool', default=False),
|
syncRegistrations=dict(type='bool', default=False),
|
||||||
trustEmail=dict(type='bool', default=False),
|
trustEmail=dict(type='bool', default=False),
|
||||||
|
@ -792,9 +845,11 @@ def main():
|
||||||
realm=dict(type='str', default='master'),
|
realm=dict(type='str', default='master'),
|
||||||
id=dict(type='str'),
|
id=dict(type='str'),
|
||||||
name=dict(type='str'),
|
name=dict(type='str'),
|
||||||
provider_id=dict(type='str', aliases=['providerId'], choices=['ldap', 'kerberos', 'sssd']),
|
provider_id=dict(type='str', aliases=['providerId']),
|
||||||
provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'),
|
provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'),
|
||||||
parent_id=dict(type='str', aliases=['parentId']),
|
parent_id=dict(type='str', aliases=['parentId']),
|
||||||
|
remove_unspecified_mappers=dict(type='bool', default=True),
|
||||||
|
bind_credential_update_mode=dict(type='str', default='always', choices=['always', 'only_indirect']),
|
||||||
mappers=dict(type='list', elements='dict', options=mapper_spec),
|
mappers=dict(type='list', elements='dict', options=mapper_spec),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -825,19 +880,26 @@ def main():
|
||||||
|
|
||||||
# Keycloak API expects config parameters to be arrays containing a single string element
|
# Keycloak API expects config parameters to be arrays containing a single string element
|
||||||
if config is not None:
|
if config is not None:
|
||||||
module.params['config'] = dict((k, [str(v).lower() if not isinstance(v, str) else v])
|
module.params['config'] = {
|
||||||
for k, v in config.items() if config[k] is not None)
|
k: [str(v).lower() if not isinstance(v, str) else v]
|
||||||
|
for k, v in config.items()
|
||||||
|
if config[k] is not None
|
||||||
|
}
|
||||||
|
|
||||||
if mappers is not None:
|
if mappers is not None:
|
||||||
for mapper in mappers:
|
for mapper in mappers:
|
||||||
if mapper.get('config') is not None:
|
if mapper.get('config') is not None:
|
||||||
mapper['config'] = dict((k, [str(v).lower() if not isinstance(v, str) else v])
|
mapper['config'] = {
|
||||||
for k, v in mapper['config'].items() if mapper['config'][k] is not None)
|
k: [str(v).lower() if not isinstance(v, str) else v]
|
||||||
|
for k, v in mapper['config'].items()
|
||||||
|
if mapper['config'][k] is not None
|
||||||
|
}
|
||||||
|
|
||||||
# Filter and map the parameters names that apply
|
# Filter and map the parameters names that apply
|
||||||
comp_params = [x for x in module.params
|
comp_params = [x for x in module.params
|
||||||
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'mappers'] and
|
if x not in list(keycloak_argument_spec().keys())
|
||||||
module.params.get(x) is not None]
|
+ ['state', 'realm', 'mappers', 'remove_unspecified_mappers', 'bind_credential_update_mode']
|
||||||
|
and module.params.get(x) is not None]
|
||||||
|
|
||||||
# See if it already exists in Keycloak
|
# See if it already exists in Keycloak
|
||||||
if cid is None:
|
if cid is None:
|
||||||
|
@ -855,7 +917,9 @@ def main():
|
||||||
|
|
||||||
# if user federation exists, get associated mappers
|
# if user federation exists, get associated mappers
|
||||||
if cid is not None and before_comp:
|
if cid is not None and before_comp:
|
||||||
before_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name'))
|
before_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '')
|
||||||
|
|
||||||
|
normalize_kc_comp(before_comp)
|
||||||
|
|
||||||
# Build a proposed changeset from parameters given to this module
|
# Build a proposed changeset from parameters given to this module
|
||||||
changeset = {}
|
changeset = {}
|
||||||
|
@ -864,7 +928,7 @@ def main():
|
||||||
new_param_value = module.params.get(param)
|
new_param_value = module.params.get(param)
|
||||||
old_value = before_comp[camel(param)] if camel(param) in before_comp else None
|
old_value = before_comp[camel(param)] if camel(param) in before_comp else None
|
||||||
if param == 'mappers':
|
if param == '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]
|
||||||
if new_param_value != old_value:
|
if new_param_value != old_value:
|
||||||
changeset[camel(param)] = new_param_value
|
changeset[camel(param)] = new_param_value
|
||||||
|
|
||||||
|
@ -873,17 +937,17 @@ def main():
|
||||||
if module.params['provider_id'] in ['kerberos', 'sssd']:
|
if module.params['provider_id'] in ['kerberos', 'sssd']:
|
||||||
module.fail_json(msg='Cannot configure mappers for {type} provider.'.format(type=module.params['provider_id']))
|
module.fail_json(msg='Cannot configure mappers for {type} provider.'.format(type=module.params['provider_id']))
|
||||||
for change in module.params['mappers']:
|
for change in module.params['mappers']:
|
||||||
change = dict((k, v) for k, v in change.items() if change[k] is not None)
|
change = {k: v for k, v in change.items() if v is not None}
|
||||||
if change.get('id') is None and change.get('name') is None:
|
if change.get('id') is None and change.get('name') is None:
|
||||||
module.fail_json(msg='Either `name` or `id` has to be specified on each mapper.')
|
module.fail_json(msg='Either `name` or `id` has to be specified on each mapper.')
|
||||||
if cid is None:
|
if cid is None:
|
||||||
old_mapper = {}
|
old_mapper = {}
|
||||||
elif change.get('id') is not None:
|
elif change.get('id') is not None:
|
||||||
old_mapper = kc.get_component(change['id'], realm)
|
old_mapper = next((before_mapper for before_mapper in before_comp.get('mappers', []) if before_mapper["id"] == change['id']), None)
|
||||||
if old_mapper is None:
|
if old_mapper is None:
|
||||||
old_mapper = {}
|
old_mapper = {}
|
||||||
else:
|
else:
|
||||||
found = kc.get_components(urlencode(dict(parent=cid, name=change['name'])), realm)
|
found = [before_mapper for before_mapper in before_comp.get('mappers', []) if before_mapper['name'] == change['name']]
|
||||||
if len(found) > 1:
|
if len(found) > 1:
|
||||||
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=change['name']))
|
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=change['name']))
|
||||||
if len(found) == 1:
|
if len(found) == 1:
|
||||||
|
@ -892,10 +956,16 @@ def main():
|
||||||
old_mapper = {}
|
old_mapper = {}
|
||||||
new_mapper = old_mapper.copy()
|
new_mapper = old_mapper.copy()
|
||||||
new_mapper.update(change)
|
new_mapper.update(change)
|
||||||
if new_mapper != old_mapper:
|
# changeset contains all desired mappers: those existing, to update or to create
|
||||||
if changeset.get('mappers') is None:
|
if changeset.get('mappers') is None:
|
||||||
changeset['mappers'] = list()
|
changeset['mappers'] = list()
|
||||||
changeset['mappers'].append(new_mapper)
|
changeset['mappers'].append(new_mapper)
|
||||||
|
changeset['mappers'] = sorted(changeset['mappers'], key=lambda x: x.get('name') or '')
|
||||||
|
|
||||||
|
# to keep unspecified existing mappers we add them to the desired mappers list, unless they're already present
|
||||||
|
if not module.params['remove_unspecified_mappers'] and 'mappers' in before_comp:
|
||||||
|
changeset_mapper_ids = [mapper['id'] for mapper in changeset['mappers'] if 'id' in mapper]
|
||||||
|
changeset['mappers'].extend([mapper for mapper in before_comp['mappers'] if mapper['id'] not in changeset_mapper_ids])
|
||||||
|
|
||||||
# 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_comp = before_comp.copy()
|
desired_comp = before_comp.copy()
|
||||||
|
@ -918,50 +988,68 @@ def main():
|
||||||
# Process a creation
|
# Process a creation
|
||||||
result['changed'] = True
|
result['changed'] = True
|
||||||
|
|
||||||
if module._diff:
|
|
||||||
result['diff'] = dict(before='', after=sanitize(desired_comp))
|
|
||||||
|
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before='', after=sanitize(desired_comp))
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
# create it
|
# create it
|
||||||
desired_comp = desired_comp.copy()
|
desired_mappers = desired_comp.pop('mappers', [])
|
||||||
updated_mappers = desired_comp.pop('mappers', [])
|
|
||||||
after_comp = kc.create_component(desired_comp, realm)
|
after_comp = kc.create_component(desired_comp, realm)
|
||||||
|
|
||||||
cid = after_comp['id']
|
cid = after_comp['id']
|
||||||
|
updated_mappers = []
|
||||||
|
# when creating a user federation, keycloak automatically creates default mappers
|
||||||
|
default_mappers = kc.get_components(urlencode(dict(parent=cid)), realm)
|
||||||
|
|
||||||
for mapper in updated_mappers:
|
# create new mappers or update existing default mappers
|
||||||
found = kc.get_components(urlencode(dict(parent=cid, name=mapper['name'])), realm)
|
for desired_mapper in desired_mappers:
|
||||||
|
found = [default_mapper for default_mapper in default_mappers if default_mapper['name'] == desired_mapper['name']]
|
||||||
if len(found) > 1:
|
if len(found) > 1:
|
||||||
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=mapper['name']))
|
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=desired_mapper['name']))
|
||||||
if len(found) == 1:
|
if len(found) == 1:
|
||||||
old_mapper = found[0]
|
old_mapper = found[0]
|
||||||
else:
|
else:
|
||||||
old_mapper = {}
|
old_mapper = {}
|
||||||
|
|
||||||
new_mapper = old_mapper.copy()
|
new_mapper = old_mapper.copy()
|
||||||
new_mapper.update(mapper)
|
new_mapper.update(desired_mapper)
|
||||||
|
|
||||||
if new_mapper.get('id') is not None:
|
if new_mapper.get('id') is not None:
|
||||||
kc.update_component(new_mapper, realm)
|
kc.update_component(new_mapper, realm)
|
||||||
|
updated_mappers.append(new_mapper)
|
||||||
else:
|
else:
|
||||||
if new_mapper.get('parentId') is None:
|
if new_mapper.get('parentId') is None:
|
||||||
new_mapper['parentId'] = after_comp['id']
|
new_mapper['parentId'] = cid
|
||||||
mapper = kc.create_component(new_mapper, realm)
|
updated_mappers.append(kc.create_component(new_mapper, realm))
|
||||||
|
|
||||||
after_comp['mappers'] = updated_mappers
|
if module.params['remove_unspecified_mappers']:
|
||||||
|
# we remove all unwanted default mappers
|
||||||
|
# we use ids so we dont accidently remove one of the previously updated default mapper
|
||||||
|
for default_mapper in default_mappers:
|
||||||
|
if not default_mapper['id'] in [x['id'] for x in updated_mappers]:
|
||||||
|
kc.delete_component(default_mapper['id'], realm)
|
||||||
|
|
||||||
|
after_comp['mappers'] = kc.get_components(urlencode(dict(parent=cid)), realm)
|
||||||
|
normalize_kc_comp(after_comp)
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before='', after=sanitize(after_comp))
|
||||||
result['end_state'] = sanitize(after_comp)
|
result['end_state'] = sanitize(after_comp)
|
||||||
|
result['msg'] = "User federation {id} has been created".format(id=cid)
|
||||||
result['msg'] = "User federation {id} has been created".format(id=after_comp['id'])
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
# Process an update
|
# Process an update
|
||||||
|
|
||||||
|
desired_copy = deepcopy(desired_comp)
|
||||||
|
before_copy = deepcopy(before_comp)
|
||||||
|
# exclude bindCredential when checking wether an update is required, therefore
|
||||||
|
# updating it only if there are other changes
|
||||||
|
if module.params['bind_credential_update_mode'] == 'only_indirect':
|
||||||
|
desired_copy.get('config', []).pop('bindCredential', None)
|
||||||
|
before_copy.get('config', []).pop('bindCredential', None)
|
||||||
# no changes
|
# no changes
|
||||||
if desired_comp == before_comp:
|
if desired_copy == before_copy:
|
||||||
result['changed'] = False
|
result['changed'] = False
|
||||||
result['end_state'] = sanitize(desired_comp)
|
result['end_state'] = sanitize(desired_comp)
|
||||||
result['msg'] = "No changes required to user federation {id}.".format(id=cid)
|
result['msg'] = "No changes required to user federation {id}.".format(id=cid)
|
||||||
|
@ -977,22 +1065,33 @@ def main():
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
# do the update
|
# do the update
|
||||||
desired_comp = desired_comp.copy()
|
desired_mappers = desired_comp.pop('mappers', [])
|
||||||
updated_mappers = desired_comp.pop('mappers', [])
|
|
||||||
kc.update_component(desired_comp, realm)
|
kc.update_component(desired_comp, realm)
|
||||||
after_comp = kc.get_component(cid, realm)
|
|
||||||
|
|
||||||
for mapper in updated_mappers:
|
for before_mapper in before_comp.get('mappers', []):
|
||||||
|
# remove unwanted existing mappers that will not be updated
|
||||||
|
if not before_mapper['id'] in [x['id'] for x in desired_mappers if 'id' in x]:
|
||||||
|
kc.delete_component(before_mapper['id'], realm)
|
||||||
|
|
||||||
|
for mapper in desired_mappers:
|
||||||
|
if mapper in before_comp.get('mappers', []):
|
||||||
|
continue
|
||||||
if mapper.get('id') is not None:
|
if mapper.get('id') is not None:
|
||||||
kc.update_component(mapper, realm)
|
kc.update_component(mapper, realm)
|
||||||
else:
|
else:
|
||||||
if mapper.get('parentId') is None:
|
if mapper.get('parentId') is None:
|
||||||
mapper['parentId'] = desired_comp['id']
|
mapper['parentId'] = desired_comp['id']
|
||||||
mapper = kc.create_component(mapper, realm)
|
kc.create_component(mapper, realm)
|
||||||
|
|
||||||
after_comp['mappers'] = updated_mappers
|
|
||||||
result['end_state'] = sanitize(after_comp)
|
|
||||||
|
|
||||||
|
after_comp = kc.get_component(cid, realm)
|
||||||
|
after_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '')
|
||||||
|
normalize_kc_comp(after_comp)
|
||||||
|
after_comp_sanitized = sanitize(after_comp)
|
||||||
|
before_comp_sanitized = sanitize(before_comp)
|
||||||
|
result['end_state'] = after_comp_sanitized
|
||||||
|
if module._diff:
|
||||||
|
result['diff'] = dict(before=before_comp_sanitized, after=after_comp_sanitized)
|
||||||
|
result['changed'] = before_comp_sanitized != after_comp_sanitized
|
||||||
result['msg'] = "User federation {id} has been updated".format(id=cid)
|
result['msg'] = "User federation {id} has been updated".format(id=cid)
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue