1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Keycloak module cleanup and consistency (#3280)

* Consistent Modules - Rename updated_?? to desired_?? in all the keycloak modules.

* Consistent Modules - Rename the comments, and add whitespace so that all the modules are a lot more consistent between each other.

* Consistent Modules - Remove final elif where a final else doesn't exist.

This is to address the inconsistency between the other modules.

Whilst I can see it being more descriptive, there should be a final "else:" to cater if the values is neither 'absent' or 'present'.

* Consistent Modules - Use dict() instead of {} like most of the other keycloak modules.

* Consistent Modules - Update keycloak authentication so that the if ordering is consistent for no-item.

* Consistent Modules - Move the 'Filter and map' process to always occur before getting an existing item.

* Consistent Modules - Be consistent with how to initialse before_?? and set it to dict() if it is None.

* Consistent Modules - Add module.exit_?? in the locations as per the other modules.

* Consistent Modules - Represent result['diff'] using dict(before=.., after=...) as per all the other modules.

* Consistent Modules - Add / Move location of when result['end_state'] is getting defined.

* Consistent modules - Add result['changed'] = False where we do nothing and exit because item exists.

* Consistent Modules - Set the value result['changed'] to True earlier so it shows up when in checking mode only.

* Consistent Modules - test for equality with a dict to assert there was no realm in the first place as per the other modules.

* Consistent Modules - Address the spelling.

* Consistent Modules - keycloak_group - Remove result['group'] as result['end_state'] is the consistent value used in the other modules.

* Consistent Modules - Order the lines in the section, Do nothing and exit consistently.

* Consistent Modules - Add result['end_state'] and still add deprecated `flow` return value.

* Consistent Modules - Add missing return documentation for `msg`.

* Consistent Modules - Tweak whitespace in the RETURN variable.

* Consistent Modules - Add result['group'] in addition to deprecated result['group'] response.

* Consistent Modules - Add return property, 'contains' to address test errors.

* Consistent Modules - Rename updated_?? to desired_?? in new modules since initial PR.

* Consistent Modules - Rename the comments, and add whitespace so that all the (recently added) modules are a lot more consistent between each other.

* Consistent Modules - Make indentation consistent within the response document.

* Consistent Modules - Use B(DEPRECATED) in a seperate line in the description.

* Consistent Modules - Add a lot of full stops to sentences.

* Consistent Modules - Use C(...) and I(...) formatting methods.

* Consistent Modules - Use "on success" everywhere for end_state response documentation.

* Consistent Modules - Update the documents for RETURN.proposed, RETURN.existing, RETURN.end_state to be the same.

* Consistent Modules - Add fragment.

* Remove period after short_description.

* Update changelog fragment.

* Consistent Modules - PRFeedback - Remove `module.exit_json(**result)` within the `Delete` section of the if statement.

There's a exit_json(..) immediately after.

* Consistent Modules - PRFeedback - Use `if not x_repr` instead of `if x_repr == dict()`.

* keycloak_authentication - Add a sample of the output.

* Replace `dict()` with `{}` for all the keycloak modules.

* Add the requested deprecated notices

* Update changelogs/fragments/3280-keycloak-module-cleanup-and-consistency.yml

Co-authored-by: Pierre Dumuid <pierre@knowyourdata.com.au>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Pierre Dumuid 2021-10-22 16:27:18 +10:30 committed by GitHub
parent 9b4b175ca3
commit 996dc617ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 593 additions and 346 deletions

View file

@ -0,0 +1,6 @@
deprecated_features:
- keycloak_authentication - the return value ``flow`` is now deprecated and will be removed in community.general 6.0.0; use ``end_state`` instead (https://github.com/ansible-collections/community.general/pull/3280).
- keycloak_group - the return value ``group`` is now deprecated and will be removed in community.general 6.0.0; use ``end_state`` instead (https://github.com/ansible-collections/community.general/pull/3280).
minor_changes:
- keycloak_* modules - refactor many of the ``keycloak_*`` modules to have similar structures, comments, and documentation (https://github.com/ansible-collections/community.general/pull/3280).

View file

@ -9,11 +9,15 @@ __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: keycloak_authentication module: keycloak_authentication
short_description: Configure authentication in Keycloak short_description: Configure authentication in Keycloak
description: description:
- This module actually can only make a copy of an existing authentication flow, add an execution to it and configure it. - This module actually can only make a copy of an existing authentication flow, add an execution to it and configure it.
- It can also delete the flow. - It can also delete the flow.
version_added: "3.3.0" version_added: "3.3.0"
options: options:
realm: realm:
description: description:
@ -79,6 +83,7 @@ options:
default: false default: false
description: description:
- If C(true), allows to remove the authentication flow and recreate it. - If C(true), allows to remove the authentication flow and recreate it.
extends_documentation_fragment: extends_documentation_fragment:
- community.general.keycloak - community.general.keycloak
@ -162,10 +167,74 @@ EXAMPLES = '''
''' '''
RETURN = ''' RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
flow: flow:
description: JSON representation for the authentication. description:
returned: on success - JSON representation for the authentication.
type: dict - Deprecated return value, it will be removed in community.general 6.0.0. Please use the return value I(end_state) instead.
returned: on success
type: dict
sample: {
"alias": "Copy of first broker login",
"authenticationExecutions": [
{
"alias": "review profile config",
"authenticationConfig": {
"alias": "review profile config",
"config": { "update.profile.on.first.login": "missing" },
"id": "6f09e4fb-aad4-496a-b873-7fa9779df6d7"
},
"configurable": true,
"displayName": "Review Profile",
"id": "8f77dab8-2008-416f-989e-88b09ccf0b4c",
"index": 0,
"level": 0,
"providerId": "idp-review-profile",
"requirement": "REQUIRED",
"requirementChoices": [ "REQUIRED", "ALTERNATIVE", "DISABLED" ]
}
],
"builtIn": false,
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"id": "bc228863-5887-4297-b898-4d988f8eaa5c",
"providerId": "basic-flow",
"topLevel": true
}
end_state:
description: Representation of the authentication after module execution.
returned: on success
type: dict
sample: {
"alias": "Copy of first broker login",
"authenticationExecutions": [
{
"alias": "review profile config",
"authenticationConfig": {
"alias": "review profile config",
"config": { "update.profile.on.first.login": "missing" },
"id": "6f09e4fb-aad4-496a-b873-7fa9779df6d7"
},
"configurable": true,
"displayName": "Review Profile",
"id": "8f77dab8-2008-416f-989e-88b09ccf0b4c",
"index": 0,
"level": 0,
"providerId": "idp-review-profile",
"requirement": "REQUIRED",
"requirementChoices": [ "REQUIRED", "ALTERNATIVE", "DISABLED" ]
}
],
"builtIn": false,
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"id": "bc228863-5887-4297-b898-4d988f8eaa5c",
"providerId": "basic-flow",
"topLevel": true
}
''' '''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak \ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak \
@ -271,9 +340,11 @@ def create_or_update_executions(kc, config, realm='master'):
def main(): def main():
""" """
Module execution Module execution
:return: :return:
""" """
argument_spec = keycloak_argument_spec() argument_spec = keycloak_argument_spec()
meta_args = dict( meta_args = dict(
realm=dict(type='str', required=True), realm=dict(type='str', required=True),
alias=dict(type='str', required=True), alias=dict(type='str', required=True),
@ -292,6 +363,7 @@ def main():
state=dict(choices=["absent", "present"], default='present'), state=dict(choices=["absent", "present"], default='present'),
force=dict(type='bool', default=False), force=dict(type='bool', default=False),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
@ -301,6 +373,7 @@ def main():
) )
result = dict(changed=False, msg='', flow={}) result = dict(changed=False, msg='', flow={})
# Obtain access token, initialize API # Obtain access token, initialize API
try: try:
connection_header = get_token(module.params) connection_header = get_token(module.params)
@ -308,6 +381,7 @@ def main():
module.fail_json(msg=str(e)) module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header) kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm') realm = module.params.get('realm')
state = module.params.get('state') state = module.params.get('state')
force = module.params.get('force') force = module.params.get('force')
@ -323,35 +397,54 @@ def main():
} }
auth_repr = kc.get_authentication_flow_by_alias(alias=new_auth_repr["alias"], realm=realm) auth_repr = kc.get_authentication_flow_by_alias(alias=new_auth_repr["alias"], realm=realm)
if auth_repr == {}: # Authentication flow does not exist
if state == 'present': # If desired state is present # Cater for when it doesn't exist (an empty dict)
if not auth_repr:
if state == 'absent':
# Do nothing and exit
if module._diff:
result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = {}
result['flow'] = result['end_state']
result['msg'] = new_auth_repr["alias"] + ' absent'
module.exit_json(**result)
elif state == 'present':
# Process a creation
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before='', after=new_auth_repr) result['diff'] = dict(before='', after=new_auth_repr)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# If copyFrom is defined, create authentication flow from a copy # If copyFrom is defined, create authentication flow from a copy
if "copyFrom" in new_auth_repr and new_auth_repr["copyFrom"] is not None: if "copyFrom" in new_auth_repr and new_auth_repr["copyFrom"] is not None:
auth_repr = kc.copy_auth_flow(config=new_auth_repr, realm=realm) auth_repr = kc.copy_auth_flow(config=new_auth_repr, realm=realm)
else: # Create an empty authentication flow else: # Create an empty authentication flow
auth_repr = kc.create_empty_auth_flow(config=new_auth_repr, realm=realm) auth_repr = kc.create_empty_auth_flow(config=new_auth_repr, realm=realm)
# If the authentication still not exist on the server, raise an exception. # If the authentication still not exist on the server, raise an exception.
if auth_repr is None: if auth_repr is None:
result['msg'] = "Authentication just created not found: " + str(new_auth_repr) result['msg'] = "Authentication just created not found: " + str(new_auth_repr)
module.fail_json(**result) module.fail_json(**result)
# Configure the executions for the flow # Configure the executions for the flow
create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm) create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm)
# Get executions created # Get executions created
exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm) exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm)
if exec_repr is not None: if exec_repr is not None:
auth_repr["authenticationExecutions"] = exec_repr auth_repr["authenticationExecutions"] = exec_repr
result['flow'] = auth_repr result['end_state'] = auth_repr
elif state == 'absent': # If desired state is absent. result['flow'] = result['end_state']
if module._diff:
result['diff'] = dict(before='', after='') else:
result['msg'] = new_auth_repr["alias"] + ' absent' if state == 'present':
else: # The authentication flow already exist # Process an update
if state == 'present': # if desired state is present
if force: # If force option is true if force: # If force option is true
# Delete the actual authentication flow # Delete the actual authentication flow
result['changed'] = True result['changed'] = True
@ -370,25 +463,35 @@ def main():
result['msg'] = "Authentication just created not found: " + str(new_auth_repr) result['msg'] = "Authentication just created not found: " + str(new_auth_repr)
module.fail_json(**result) module.fail_json(**result)
# Configure the executions for the flow # Configure the executions for the flow
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
changed, diff = create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm) changed, diff = create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm)
result['changed'] |= changed result['changed'] |= changed
if module._diff: if module._diff:
result['diff'] = diff result['diff'] = diff
# Get executions created # Get executions created
exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm) exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm)
if exec_repr is not None: if exec_repr is not None:
auth_repr["authenticationExecutions"] = exec_repr auth_repr["authenticationExecutions"] = exec_repr
result['flow'] = auth_repr result['end_state'] = auth_repr
elif state == 'absent': # If desired state is absent result['flow'] = result['end_state']
else:
# Process a deletion (because state was not 'present')
result['changed'] = True result['changed'] = True
# Delete the authentication flow alias.
if module._diff: if module._diff:
result['diff'] = dict(before=auth_repr, after='') result['diff'] = dict(before=auth_repr, after='')
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete it
kc.delete_authentication_flow_by_id(id=auth_repr["id"], realm=realm) kc.delete_authentication_flow_by_id(id=auth_repr["id"], realm=realm)
result['msg'] = 'Authentication flow: {alias} id: {id} is deleted'.format(alias=new_auth_repr['alias'], result['msg'] = 'Authentication flow: {alias} id: {id} is deleted'.format(alias=new_auth_repr['alias'],
id=auth_repr["id"]) id=auth_repr["id"])

View file

@ -62,17 +62,17 @@ options:
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 I(client_id)).
type: str type: str
description: description:
description: description:
- Description of the client in Keycloak - Description of the client in Keycloak.
type: str type: str
root_url: root_url:
description: description:
- Root URL appended to relative URLs for this client - Root URL appended to relative URLs for this client.
This is 'rootUrl' in the Keycloak REST API. This is 'rootUrl' in the Keycloak REST API.
aliases: aliases:
- rootUrl - rootUrl
@ -80,7 +80,7 @@ options:
admin_url: admin_url:
description: description:
- URL to the admin interface of the client - URL to the admin interface of the client.
This is 'adminUrl' in the Keycloak REST API. This is 'adminUrl' in the Keycloak REST API.
aliases: aliases:
- adminUrl - adminUrl
@ -357,7 +357,7 @@ options:
protocol: protocol:
description: description:
- This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper - This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper.
is active. is active.
choices: ['openid-connect', 'saml'] choices: ['openid-connect', 'saml']
type: str type: str
@ -513,7 +513,6 @@ options:
extends_documentation_fragment: extends_documentation_fragment:
- community.general.keycloak - community.general.keycloak
author: author:
- Eike Frost (@eikef) - Eike Frost (@eikef)
''' '''
@ -645,20 +644,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Client testclient has been updated" sample: "Client testclient has been updated"
proposed: proposed:
description: client representation of proposed changes to client description: Representation of proposed client.
returned: always returned: always
type: dict type: dict
sample: { sample: {
clientId: "test" clientId: "test"
} }
existing: existing:
description: client representation of existing client (sample is truncated) description: Representation of existing client (sample is truncated).
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -667,9 +667,10 @@ existing:
"request.object.signature.alg": "RS256", "request.object.signature.alg": "RS256",
} }
} }
end_state: end_state:
description: client representation of client after module execution (sample is truncated) description: Representation of client after module execution (sample is truncated).
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"adminUrl": "http://www.example.com/admin_url", "adminUrl": "http://www.example.com/admin_url",
@ -685,7 +686,7 @@ from ansible.module_utils.basic import AnsibleModule
def sanitize_cr(clientrep): def sanitize_cr(clientrep):
""" Removes probably sensitive details from a client representation """ Removes probably sensitive details from a client representation.
:param clientrep: the clientrep dict to be sanitized :param clientrep: the clientrep dict to be sanitized
:return: sanitized clientrep dict :return: sanitized clientrep dict
@ -759,6 +760,7 @@ def main():
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']),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
@ -781,12 +783,12 @@ def main():
cid = module.params.get('id') cid = module.params.get('id')
state = module.params.get('state') state = module.params.get('state')
# convert module parameters to client representation parameters (if they belong in there) # Filter and map the parameters names that apply to the client
client_params = [x for x in module.params client_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None] module.params.get(x) is not None]
keycloak_argument_spec().keys()
# See whether the client already exists in Keycloak # See if it already exists in Keycloak
if cid is None: if cid is None:
before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm) before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm)
if before_client is not None: if before_client is not None:
@ -795,10 +797,10 @@ def main():
before_client = kc.get_client_by_id(cid, realm=realm) before_client = kc.get_client_by_id(cid, realm=realm)
if before_client is None: if before_client is None:
before_client = dict() before_client = {}
# Build a proposed changeset from parameters given to this module # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for client_param in client_params: for client_param in client_params:
new_param_value = module.params.get(client_param) new_param_value = module.params.get(client_param)
@ -817,54 +819,61 @@ def main():
changeset[camel(client_param)] = new_param_value changeset[camel(client_param)] = new_param_value
# Whether creating or updating a client, take the before-state and merge the changeset into it # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_client = before_client.copy() desired_client = before_client.copy()
updated_client.update(changeset) desired_client.update(changeset)
result['proposed'] = sanitize_cr(changeset) result['proposed'] = sanitize_cr(changeset)
result['existing'] = sanitize_cr(before_client) result['existing'] = sanitize_cr(before_client)
# If the client does not exist yet, before_client is still empty # Cater for when it doesn't exist (an empty dict)
if before_client == dict(): if not before_client:
if state == 'absent': if state == 'absent':
# do nothing and exit # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['msg'] = 'Client does not exist, doing nothing.' result['changed'] = False
result['end_state'] = {}
result['msg'] = 'Client does not exist; doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# create new client # Process a creation
result['changed'] = True result['changed'] = True
if 'clientId' not in updated_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 module._diff: if module._diff:
result['diff'] = dict(before='', after=sanitize_cr(updated_client)) result['diff'] = dict(before='', after=sanitize_cr(desired_client))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.create_client(updated_client, realm=realm) # create it
after_client = kc.get_client_by_clientid(updated_client['clientId'], realm=realm) kc.create_client(desired_client, realm=realm)
after_client = kc.get_client_by_clientid(desired_client['clientId'], realm=realm)
result['end_state'] = sanitize_cr(after_client) result['end_state'] = sanitize_cr(after_client)
result['msg'] = 'Client %s has been created.' % updated_client['clientId'] result['msg'] = 'Client %s has been created.' % desired_client['clientId']
module.exit_json(**result) module.exit_json(**result)
else: else:
if state == 'present': if state == 'present':
# update existing client # Process an update
result['changed'] = True result['changed'] = True
if module.check_mode: if module.check_mode:
# We can only compare the current client with the proposed updates we have # We can only compare the current client with the proposed updates we have
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize_cr(before_client), result['diff'] = dict(before=sanitize_cr(before_client),
after=sanitize_cr(updated_client)) after=sanitize_cr(desired_client))
result['changed'] = (before_client != updated_client) result['changed'] = (before_client != desired_client)
module.exit_json(**result) module.exit_json(**result)
kc.update_client(cid, updated_client, realm=realm) # do the update
kc.update_client(cid, desired_client, realm=realm)
after_client = kc.get_client_by_id(cid, realm=realm) after_client = kc.get_client_by_id(cid, realm=realm)
if before_client == after_client: if before_client == after_client:
@ -872,25 +881,29 @@ def main():
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize_cr(before_client), result['diff'] = dict(before=sanitize_cr(before_client),
after=sanitize_cr(after_client)) after=sanitize_cr(after_client))
result['end_state'] = sanitize_cr(after_client) result['end_state'] = sanitize_cr(after_client)
result['msg'] = 'Client %s has been updated.' % updated_client['clientId'] result['msg'] = 'Client %s has been updated.' % desired_client['clientId']
module.exit_json(**result) module.exit_json(**result)
else: else:
# Delete existing client # Process a deletion (because state was not 'present')
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff']['before'] = sanitize_cr(before_client) result['diff'] = dict(before=sanitize_cr(before_client), after='')
result['diff']['after'] = ''
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete it
kc.delete_client(cid, realm=realm) kc.delete_client(cid, realm=realm)
result['proposed'] = dict() result['proposed'] = {}
result['end_state'] = dict()
result['end_state'] = {}
result['msg'] = 'Client %s has been deleted.' % before_client['clientId'] result['msg'] = 'Client %s has been deleted.' % before_client['clientId']
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)

View file

@ -11,6 +11,7 @@ DOCUMENTATION = '''
module: keycloak_client_rolemapping module: keycloak_client_rolemapping
short_description: Allows administration of Keycloak client_rolemapping with the Keycloak API short_description: Allows administration of Keycloak client_rolemapping with the Keycloak API
version_added: 3.5.0 version_added: 3.5.0
description: description:
@ -158,21 +159,22 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Role role1 assigned to group group1." sample: "Role role1 assigned to group group1."
proposed: proposed:
description: role_representation representation of proposed changes to client_rolemapping. description: Representation of proposed client role mapping.
returned: always returned: always
type: dict type: dict
sample: { sample: {
clientId: "test" clientId: "test"
} }
existing: existing:
description: description:
- role_representation representation of existing role_representation. - Representation of existing client role mapping.
- The sample is truncated. - The sample is truncated.
returned: always returned: always
type: dict type: dict
@ -182,11 +184,12 @@ existing:
"request.object.signature.alg": "RS256", "request.object.signature.alg": "RS256",
} }
} }
end_state: end_state:
description: description:
- role_representation representation of role_representation after module execution. - Representation of client role mapping after module execution.
- The sample is truncated. - The sample is truncated.
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"adminUrl": "http://www.example.com/admin_url", "adminUrl": "http://www.example.com/admin_url",

View file

@ -86,7 +86,7 @@ options:
suboptions: suboptions:
protocol: protocol:
description: description:
- This specifies for which protocol this protocol mapper - This specifies for which protocol this protocol mapper.
- is active. - is active.
choices: ['openid-connect', 'saml', 'wsfed'] choices: ['openid-connect', 'saml', 'wsfed']
type: str type: str
@ -256,20 +256,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Client_scope testclientscope has been updated" sample: "Client_scope testclientscope has been updated"
proposed: proposed:
description: client_scope representation of proposed changes to client_scope description: Representation of proposed client scope.
returned: always returned: always
type: dict type: dict
sample: { sample: {
clientId: "test" clientId: "test"
} }
existing: existing:
description: client_scope representation of existing client_scope (sample is truncated) description: Representation of existing client scope (sample is truncated).
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -278,9 +279,10 @@ existing:
"request.object.signature.alg": "RS256", "request.object.signature.alg": "RS256",
} }
} }
end_state: end_state:
description: client_scope representation of client_scope after module execution (sample is truncated) description: Representation of client scope after module execution (sample is truncated).
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"adminUrl": "http://www.example.com/admin_url", "adminUrl": "http://www.example.com/admin_url",
@ -296,7 +298,7 @@ from ansible.module_utils.basic import AnsibleModule
def sanitize_cr(clientscoperep): def sanitize_cr(clientscoperep):
""" Removes probably sensitive details from a clientscoperep representation """ Removes probably sensitive details from a clientscoperep representation.
:param clientscoperep: the clientscoperep dict to be sanitized :param clientscoperep: the clientscoperep dict to be sanitized
:return: sanitized clientrep dict :return: sanitized clientrep dict
@ -361,22 +363,22 @@ def main():
name = module.params.get('name') name = module.params.get('name')
protocol_mappers = module.params.get('protocol_mappers') protocol_mappers = module.params.get('protocol_mappers')
before_clientscope = None # current state of the clientscope, for merging. # Filter and map the parameters names that apply to the client scope
clientscope_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None]
# does the clientscope already exist? # See if it already exists in Keycloak
if cid is None: if cid is None:
before_clientscope = kc.get_clientscope_by_name(name, realm=realm) before_clientscope = kc.get_clientscope_by_name(name, realm=realm)
else: else:
before_clientscope = kc.get_clientscope_by_clientscopeid(cid, realm=realm) before_clientscope = kc.get_clientscope_by_clientscopeid(cid, realm=realm)
before_clientscope = {} if before_clientscope is None else before_clientscope if before_clientscope is None:
before_clientscope = {}
clientscope_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None]
# Build a proposed changeset from parameters given to this module # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for clientscope_param in clientscope_params: for clientscope_param in clientscope_params:
new_param_value = module.params.get(clientscope_param) new_param_value = module.params.get(clientscope_param)
@ -394,81 +396,87 @@ def main():
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 = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value]
changeset[camel(clientscope_param)] = new_param_value changeset[camel(clientscope_param)] = new_param_value
# prepare the new clientscope # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_clientscope = before_clientscope.copy() desired_clientscope = before_clientscope.copy()
updated_clientscope.update(changeset) desired_clientscope.update(changeset)
# if before_clientscope is none, the clientscope doesn't exist. # Cater for when it doesn't exist (an empty dict)
if before_clientscope == {}: if not before_clientscope:
if state == 'absent': if state == 'absent':
# nothing to do. # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = {}
result['msg'] = 'Clientscope does not exist; doing nothing.' result['msg'] = 'Clientscope does not exist; doing nothing.'
result['end_state'] = dict()
module.exit_json(**result) module.exit_json(**result)
# for 'present', create a new clientscope. # Process a creation
result['changed'] = True result['changed'] = True
if name is None: if name is None:
module.fail_json(msg='name must be specified when creating a new clientscope') module.fail_json(msg='name must be specified when creating a new clientscope')
if module._diff: if module._diff:
result['diff'] = dict(before='', after=sanitize_cr(updated_clientscope)) result['diff'] = dict(before='', after=sanitize_cr(desired_clientscope))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do it for real! # create it
kc.create_clientscope(updated_clientscope, realm=realm) kc.create_clientscope(desired_clientscope, realm=realm)
after_clientscope = kc.get_clientscope_by_name(name, realm) after_clientscope = kc.get_clientscope_by_name(name, realm)
result['end_state'] = sanitize_cr(after_clientscope) result['end_state'] = sanitize_cr(after_clientscope)
result['msg'] = 'Clientscope {name} has been created with ID {id}'.format(name=after_clientscope['name'], result['msg'] = 'Clientscope {name} has been created with ID {id}'.format(name=after_clientscope['name'],
id=after_clientscope['id']) id=after_clientscope['id'])
else: else:
if state == 'present': if state == 'present':
# Process an update
# no changes # no changes
if updated_clientscope == before_clientscope: if desired_clientscope == before_clientscope:
result['changed'] = False result['changed'] = False
result['end_state'] = sanitize_cr(updated_clientscope) result['end_state'] = sanitize_cr(desired_clientscope)
result['msg'] = "No changes required to clientscope {name}.".format(name=before_clientscope['name']) result['msg'] = "No changes required to clientscope {name}.".format(name=before_clientscope['name'])
module.exit_json(**result) module.exit_json(**result)
# update the existing clientscope # doing an update
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize_cr(before_clientscope), after=sanitize_cr(updated_clientscope)) result['diff'] = dict(before=sanitize_cr(before_clientscope), after=sanitize_cr(desired_clientscope))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do the clientscope update # do the update
kc.update_clientscope(updated_clientscope, realm=realm) kc.update_clientscope(desired_clientscope, realm=realm)
# do the protocolmappers update # do the protocolmappers update
if protocol_mappers is not None: if protocol_mappers is not None:
for protocol_mapper in protocol_mappers: for protocol_mapper in protocol_mappers:
# update if protocolmapper exist # update if protocolmapper exist
current_protocolmapper = kc.get_clientscope_protocolmapper_by_name(updated_clientscope['id'], protocol_mapper['name'], realm=realm) current_protocolmapper = kc.get_clientscope_protocolmapper_by_name(desired_clientscope['id'], protocol_mapper['name'], realm=realm)
if current_protocolmapper is not None: if current_protocolmapper is not None:
protocol_mapper['id'] = current_protocolmapper['id'] protocol_mapper['id'] = current_protocolmapper['id']
kc.update_clientscope_protocolmappers(updated_clientscope['id'], protocol_mapper, realm=realm) kc.update_clientscope_protocolmappers(desired_clientscope['id'], protocol_mapper, realm=realm)
# create otherwise # create otherwise
else: else:
kc.create_clientscope_protocolmapper(updated_clientscope['id'], protocol_mapper, realm=realm) kc.create_clientscope_protocolmapper(desired_clientscope['id'], protocol_mapper, realm=realm)
after_clientscope = kc.get_clientscope_by_clientscopeid(updated_clientscope['id'], realm=realm) after_clientscope = kc.get_clientscope_by_clientscopeid(desired_clientscope['id'], realm=realm)
result['end_state'] = after_clientscope result['end_state'] = after_clientscope
result['msg'] = "Clientscope {id} has been updated".format(id=after_clientscope['id'])
result['msg'] = "Clientscope {id} has been updated".format(id=after_clientscope['id'])
module.exit_json(**result) module.exit_json(**result)
elif state == 'absent': else:
result['end_state'] = dict() # Process a deletion (because state was not 'present')
result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize_cr(before_clientscope), after='') result['diff'] = dict(before=sanitize_cr(before_clientscope), after='')
@ -476,14 +484,13 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete for real # delete it
cid = before_clientscope['id'] cid = before_clientscope['id']
kc.delete_clientscope(cid=cid, realm=realm) kc.delete_clientscope(cid=cid, realm=realm)
result['changed'] = True result['end_state'] = {}
result['msg'] = "Clientscope {name} has been deleted".format(name=before_clientscope['name'])
module.exit_json(**result) result['msg'] = "Clientscope {name} has been deleted".format(name=before_clientscope['name'])
module.exit_json(**result) module.exit_json(**result)

View file

@ -31,7 +31,7 @@ description:
options: options:
state: state:
description: description:
- State of the client template - State of the client template.
- On C(present), the client template will be created (or updated if it exists already). - On C(present), the client template will be created (or updated if it exists already).
- On C(absent), the client template will be removed if it exists - On C(absent), the client template will be removed if it exists
choices: ['present', 'absent'] choices: ['present', 'absent']
@ -51,12 +51,12 @@ options:
name: name:
description: description:
- Name of the client template - Name of the client template.
type: str type: str
description: description:
description: description:
- Description of the client template in Keycloak - Description of the client template in Keycloak.
type: str type: str
protocol: protocol:
@ -100,7 +100,7 @@ options:
protocol: protocol:
description: description:
- is either 'openid-connect' or 'saml', this specifies for which protocol this protocol mapper - This is either C(openid-connect) or C(saml), this specifies for which protocol this protocol mapper.
is active. is active.
choices: ['openid-connect', 'saml'] choices: ['openid-connect', 'saml']
type: str type: str
@ -143,7 +143,7 @@ options:
contents differ depending on the value of I(protocolMapper) and are not documented contents differ depending on the value of I(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 "existing" field. protocol mapper configuration through check-mode in the I(existing) field.
type: dict type: dict
attributes: attributes:
@ -163,7 +163,6 @@ notes:
extends_documentation_fragment: extends_documentation_fragment:
- community.general.keycloak - community.general.keycloak
author: author:
- Eike Frost (@eikef) - Eike Frost (@eikef)
''' '''
@ -231,20 +230,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Client template testclient has been updated" sample: "Client template testclient has been updated"
proposed: proposed:
description: client template representation of proposed changes to client template description: Representation of proposed client template.
returned: always returned: always
type: dict type: dict
sample: { sample: {
name: "test01" name: "test01"
} }
existing: existing:
description: client template representation of existing client template (sample is truncated) description: Representation of existing client template (sample is truncated).
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -254,9 +254,10 @@ existing:
"name": "test01", "name": "test01",
"protocol": "saml" "protocol": "saml"
} }
end_state: end_state:
description: client template representation of client template after module execution (sample is truncated) description: Representation of client template after module execution (sample is truncated).
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"description": "test01", "description": "test01",
@ -302,6 +303,7 @@ def main():
full_scope_allowed=dict(type='bool'), full_scope_allowed=dict(type='bool'),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec), protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
@ -317,19 +319,20 @@ def main():
connection_header = get_token(module.params) connection_header = get_token(module.params)
except KeycloakError as e: except KeycloakError as e:
module.fail_json(msg=str(e)) module.fail_json(msg=str(e))
kc = KeycloakAPI(module, connection_header) kc = KeycloakAPI(module, connection_header)
realm = module.params.get('realm') realm = module.params.get('realm')
state = module.params.get('state') state = module.params.get('state')
cid = module.params.get('id') cid = module.params.get('id')
# convert module parameters to client representation parameters (if they belong in there) # Filter and map the parameters names that apply to the client template
clientt_params = [x for x in module.params clientt_params = [x for x in module.params
if x not in ['state', 'auth_keycloak_url', 'auth_client_id', 'auth_realm', if x not in ['state', 'auth_keycloak_url', 'auth_client_id', 'auth_realm',
'auth_client_secret', 'auth_username', 'auth_password', 'auth_client_secret', 'auth_username', 'auth_password',
'validate_certs', 'realm'] and module.params.get(x) is not None] 'validate_certs', 'realm'] and module.params.get(x) is not None]
# See whether the client template already exists in Keycloak # See if it already exists in Keycloak
if cid is None: if cid is None:
before_clientt = kc.get_client_template_by_name(module.params.get('name'), realm=realm) before_clientt = kc.get_client_template_by_name(module.params.get('name'), realm=realm)
if before_clientt is not None: if before_clientt is not None:
@ -338,12 +341,12 @@ def main():
before_clientt = kc.get_client_template_by_id(cid, realm=realm) before_clientt = kc.get_client_template_by_id(cid, realm=realm)
if before_clientt is None: if before_clientt is None:
before_clientt = dict() before_clientt = {}
result['existing'] = before_clientt result['existing'] = before_clientt
# Build a proposed changeset from parameters given to this module # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for clientt_param in clientt_params: for clientt_param in clientt_params:
# lists in the Keycloak API are sorted # lists in the Keycloak API are sorted
@ -355,78 +358,89 @@ def main():
pass pass
changeset[camel(clientt_param)] = new_param_value changeset[camel(clientt_param)] = new_param_value
# Whether creating or updating a client, take the before-state and merge the changeset into it # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_clientt = before_clientt.copy() desired_clientt = before_clientt.copy()
updated_clientt.update(changeset) desired_clientt.update(changeset)
result['proposed'] = changeset result['proposed'] = changeset
# If the client template does not exist yet, before_client is still empty # Cater for when it doesn't exist (an empty dict)
if before_clientt == dict(): if not before_clientt:
if state == 'absent': if state == 'absent':
# do nothing and exit # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = {}
result['msg'] = 'Client template does not exist, doing nothing.' result['msg'] = 'Client template does not exist, doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# create new client template # Process a creation
result['changed'] = True result['changed'] = True
if 'name' not in updated_clientt:
if 'name' not in desired_clientt:
module.fail_json(msg='name needs to be specified when creating a new client') module.fail_json(msg='name needs to be specified when creating a new client')
if module._diff: if module._diff:
result['diff'] = dict(before='', after=updated_clientt) result['diff'] = dict(before='', after=desired_clientt)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.create_client_template(updated_clientt, realm=realm) # create it
after_clientt = kc.get_client_template_by_name(updated_clientt['name'], realm=realm) kc.create_client_template(desired_clientt, realm=realm)
after_clientt = kc.get_client_template_by_name(desired_clientt['name'], realm=realm)
result['end_state'] = after_clientt result['end_state'] = after_clientt
result['msg'] = 'Client template %s has been created.' % updated_clientt['name'] result['msg'] = 'Client template %s has been created.' % desired_clientt['name']
module.exit_json(**result) module.exit_json(**result)
else: else:
if state == 'present': if state == 'present':
# update existing client template # Process an update
result['changed'] = True result['changed'] = True
if module.check_mode: if module.check_mode:
# We can only compare the current client template with the proposed updates we have # We can only compare the current client template with the proposed updates we have
if module._diff: if module._diff:
result['diff'] = dict(before=before_clientt, result['diff'] = dict(before=before_clientt,
after=updated_clientt) after=desired_clientt)
module.exit_json(**result) module.exit_json(**result)
kc.update_client_template(cid, updated_clientt, realm=realm) # do the update
kc.update_client_template(cid, desired_clientt, realm=realm)
after_clientt = kc.get_client_template_by_id(cid, realm=realm) after_clientt = kc.get_client_template_by_id(cid, realm=realm)
if before_clientt == after_clientt: if before_clientt == after_clientt:
result['changed'] = False result['changed'] = False
if module._diff:
result['diff'] = dict(before=before_clientt,
after=after_clientt)
result['end_state'] = after_clientt result['end_state'] = after_clientt
result['msg'] = 'Client template %s has been updated.' % updated_clientt['name']
module.exit_json(**result)
else:
# Delete existing client
result['changed'] = True
if module._diff: if module._diff:
result['diff']['before'] = before_clientt result['diff'] = dict(before=before_clientt, after=after_clientt)
result['diff']['after'] = ''
result['msg'] = 'Client template %s has been updated.' % desired_clientt['name']
module.exit_json(**result)
else:
# Process a deletion (because state was not 'present')
result['changed'] = True
if module._diff:
result['diff'] = dict(before=before_clientt, after='')
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete it
kc.delete_client_template(cid, realm=realm) kc.delete_client_template(cid, realm=realm)
result['proposed'] = dict() result['proposed'] = {}
result['end_state'] = dict()
result['end_state'] = {}
result['msg'] = 'Client template %s has been deleted.' % before_clientt['name'] result['msg'] = 'Client template %s has been deleted.' % before_clientt['name']
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)

View file

@ -159,34 +159,91 @@ EXAMPLES = '''
''' '''
RETURN = ''' RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
end_state:
description: Representation of the group after module execution (sample is truncated).
returned: on success
type: complex
contains:
id:
description: GUID that identifies the group.
type: str
returned: always
sample: 23f38145-3195-462c-97e7-97041ccea73e
name:
description: Name of the group.
type: str
returned: always
sample: grp-test-123
attributes:
description: Attributes applied to this group.
type: dict
returned: always
sample:
attr1: ["val1", "val2", "val3"]
path:
description: URI path to the group.
type: str
returned: always
sample: /grp-test-123
realmRoles:
description: An array of the realm-level roles granted to this group.
type: list
returned: always
sample: []
subGroups:
description: A list of groups that are children of this group. These groups will have the same parameters as
documented here.
type: list
returned: always
clientRoles:
description: A list of client-level roles granted to this group.
type: list
returned: always
sample: []
access:
description: A dict describing the accesses you have to this group based on the credentials used.
type: dict
returned: always
sample:
manage: true
manageMembership: true
view: true
group: group:
description: Group representation of the group after module execution (sample is truncated). description:
- Representation of the group after module execution.
- Deprecated return value, it will be removed in community.general 6.0.0. Please use the return value I(end_state) instead.
returned: always returned: always
type: complex type: complex
contains: contains:
id: id:
description: GUID that identifies the group description: GUID that identifies the group.
type: str type: str
returned: always returned: always
sample: 23f38145-3195-462c-97e7-97041ccea73e sample: 23f38145-3195-462c-97e7-97041ccea73e
name: name:
description: Name of the group description: Name of the group.
type: str type: str
returned: always returned: always
sample: grp-test-123 sample: grp-test-123
attributes: attributes:
description: Attributes applied to this group description: Attributes applied to this group.
type: dict type: dict
returned: always returned: always
sample: sample:
attr1: ["val1", "val2", "val3"] attr1: ["val1", "val2", "val3"]
path: path:
description: URI path to the group description: URI path to the group.
type: str type: str
returned: always returned: always
sample: /grp-test-123 sample: /grp-test-123
realmRoles: realmRoles:
description: An array of the realm-level roles granted to this group description: An array of the realm-level roles granted to this group.
type: list type: list
returned: always returned: always
sample: [] sample: []
@ -196,7 +253,7 @@ group:
type: list type: list
returned: always returned: always
clientRoles: clientRoles:
description: A list of client-level roles granted to this group description: A list of client-level roles granted to this group.
type: list type: list
returned: always returned: always
sample: [] sample: []
@ -208,6 +265,7 @@ group:
manage: true manage: true
manageMembership: true manageMembership: true
view: true view: true
''' '''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
@ -222,6 +280,7 @@ def main():
:return: :return:
""" """
argument_spec = keycloak_argument_spec() argument_spec = keycloak_argument_spec()
meta_args = dict( meta_args = dict(
state=dict(default='present', choices=['present', 'absent']), state=dict(default='present', choices=['present', 'absent']),
realm=dict(default='master'), realm=dict(default='master'),
@ -254,16 +313,6 @@ def main():
name = module.params.get('name') name = module.params.get('name')
attributes = module.params.get('attributes') attributes = module.params.get('attributes')
before_group = None # current state of the group, for merging.
# does the group already exist?
if gid is None:
before_group = kc.get_group_by_name(name, realm=realm)
else:
before_group = kc.get_group_by_groupid(gid, realm=realm)
before_group = {} if before_group is None else before_group
# attributes in Keycloak have their values returned as lists # attributes in Keycloak have their values returned as lists
# via the API. attributes is a dict, so we'll transparently convert # via the API. attributes is a dict, so we'll transparently convert
# the values to lists. # the values to lists.
@ -271,81 +320,103 @@ def main():
for key, val in module.params['attributes'].items(): for key, val in module.params['attributes'].items():
module.params['attributes'][key] = [val] if not isinstance(val, list) else val module.params['attributes'][key] = [val] if not isinstance(val, list) else val
# Filter and map the parameters names that apply to the group
group_params = [x for x in module.params group_params = [x for x in module.params
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and
module.params.get(x) is not None] module.params.get(x) is not None]
# build a changeset # See if it already exists in Keycloak
if gid is None:
before_group = kc.get_group_by_name(name, realm=realm)
else:
before_group = kc.get_group_by_groupid(gid, realm=realm)
if before_group is None:
before_group = {}
# Build a proposed changeset from parameters given to this module
changeset = {} changeset = {}
for param in group_params: for param in group_params:
new_param_value = module.params.get(param) new_param_value = module.params.get(param)
old_value = before_group[param] if param in before_group else None old_value = before_group[param] if param in before_group else None
if new_param_value != old_value: if new_param_value != old_value:
changeset[camel(param)] = new_param_value changeset[camel(param)] = new_param_value
# prepare the new group # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_group = before_group.copy() desired_group = before_group.copy()
updated_group.update(changeset) desired_group.update(changeset)
# if before_group is none, the group doesn't exist. # Cater for when it doesn't exist (an empty dict)
if before_group == {}: if not before_group:
if state == 'absent': if state == 'absent':
# nothing to do. # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = {}
result['group'] = result['end_state']
result['msg'] = 'Group does not exist; doing nothing.' result['msg'] = 'Group does not exist; doing nothing.'
result['group'] = dict()
module.exit_json(**result) module.exit_json(**result)
# for 'present', create a new group. # Process a creation
result['changed'] = True result['changed'] = True
if name is None: if name is None:
module.fail_json(msg='name must be specified when creating a new group') module.fail_json(msg='name must be specified when creating a new group')
if module._diff: if module._diff:
result['diff'] = dict(before='', after=updated_group) result['diff'] = dict(before='', after=desired_group)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do it for real! # create it
kc.create_group(updated_group, realm=realm) kc.create_group(desired_group, realm=realm)
after_group = kc.get_group_by_name(name, realm) after_group = kc.get_group_by_name(name, realm)
result['group'] = after_group result['end_state'] = after_group
result['group'] = result['end_state']
result['msg'] = 'Group {name} has been created with ID {id}'.format(name=after_group['name'], result['msg'] = 'Group {name} has been created with ID {id}'.format(name=after_group['name'],
id=after_group['id']) id=after_group['id'])
module.exit_json(**result)
else: else:
if state == 'present': if state == 'present':
# Process an update
# no changes # no changes
if updated_group == before_group: if desired_group == before_group:
result['changed'] = False result['changed'] = False
result['group'] = updated_group result['end_state'] = desired_group
result['group'] = result['end_state']
result['msg'] = "No changes required to group {name}.".format(name=before_group['name']) result['msg'] = "No changes required to group {name}.".format(name=before_group['name'])
module.exit_json(**result) module.exit_json(**result)
# update the existing group # doing an update
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=before_group, after=updated_group) result['diff'] = dict(before=before_group, after=desired_group)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do the update # do the update
kc.update_group(updated_group, realm=realm) kc.update_group(desired_group, realm=realm)
after_group = kc.get_group_by_groupid(updated_group['id'], realm=realm) after_group = kc.get_group_by_groupid(desired_group['id'], realm=realm)
result['end_state'] = after_group
result['group'] = result['end_state']
result['group'] = after_group
result['msg'] = "Group {id} has been updated".format(id=after_group['id']) result['msg'] = "Group {id} has been updated".format(id=after_group['id'])
module.exit_json(**result) module.exit_json(**result)
elif state == 'absent': else:
result['group'] = dict() # Process a deletion (because state was not 'present')
result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=before_group, after='') result['diff'] = dict(before=before_group, after='')
@ -353,14 +424,14 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete for real # delete it
gid = before_group['id'] gid = before_group['id']
kc.delete_group(groupid=gid, realm=realm) kc.delete_group(groupid=gid, realm=realm)
result['changed'] = True result['end_state'] = {}
result['msg'] = "Group {name} has been deleted".format(name=before_group['name']) result['group'] = result['end_state']
module.exit_json(**result) result['msg'] = "Group {name} has been deleted".format(name=before_group['name'])
module.exit_json(**result) module.exit_json(**result)

View file

@ -339,13 +339,13 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Identity provider my-idp has been created" sample: "Identity provider my-idp has been created"
proposed: proposed:
description: Representation of proposed changes to identity provider description: Representation of proposed identity provider.
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -363,7 +363,7 @@ proposed:
} }
existing: existing:
description: Representation of existing identity provider description: Representation of existing identity provider.
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -391,8 +391,8 @@ existing:
} }
end_state: end_state:
description: Representation of identity provider after module execution description: Representation of identity provider after module execution.
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"addReadTokenRoleOnCreate": false, "addReadTokenRoleOnCreate": false,
@ -416,7 +416,6 @@ end_state:
"storeToken": false, "storeToken": false,
"trustEmail": false, "trustEmail": false,
} }
''' '''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
@ -438,7 +437,7 @@ def get_identity_provider_with_mappers(kc, alias, realm):
if idp is not None: if idp is not None:
idp['mappers'] = sorted(kc.get_identity_provider_mappers(alias, realm), key=lambda x: x.get('name')) idp['mappers'] = sorted(kc.get_identity_provider_mappers(alias, realm), key=lambda x: x.get('name'))
if idp is None: if idp is None:
idp = dict() idp = {}
return idp return idp
@ -497,16 +496,16 @@ def main():
alias = module.params.get('alias') alias = module.params.get('alias')
state = module.params.get('state') state = module.params.get('state')
# convert module parameters to client representation parameters (if they belong in there) # Filter and map the parameters names that apply to the identity provider.
idp_params = [x for x in module.params idp_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()) + ['state', 'realm', 'mappers'] and
module.params.get(x) is not None] module.params.get(x) is not None]
# does the identity provider already exist? # See if it already exists in Keycloak
before_idp = get_identity_provider_with_mappers(kc, alias, realm) before_idp = get_identity_provider_with_mappers(kc, alias, realm)
# build a changeset # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for param in idp_params: for param in idp_params:
new_param_value = module.params.get(param) new_param_value = module.params.get(param)
@ -539,37 +538,37 @@ def main():
changeset['mappers'] = list() changeset['mappers'] = list()
changeset['mappers'].append(new_mapper) changeset['mappers'].append(new_mapper)
# prepare the new representation # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_idp = before_idp.copy() desired_idp = before_idp.copy()
updated_idp.update(changeset) desired_idp.update(changeset)
result['proposed'] = sanitize(changeset) result['proposed'] = sanitize(changeset)
result['existing'] = sanitize(before_idp) result['existing'] = sanitize(before_idp)
# if before_idp is none, the identity provider doesn't exist. # Cater for when it doesn't exist (an empty dict)
if before_idp == dict(): if not before_idp:
if state == 'absent': if state == 'absent':
# nothing to do. # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False result['changed'] = False
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = 'Identity provider does not exist; doing nothing.' result['msg'] = 'Identity provider does not exist; doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# for 'present', create a new identity provider. # Process a creation
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before='', after=sanitize(updated_idp)) result['diff'] = dict(before='', after=sanitize(desired_idp))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do it for real! # create it
updated_idp = updated_idp.copy() desired_idp = desired_idp.copy()
mappers = updated_idp.pop('mappers', []) mappers = desired_idp.pop('mappers', [])
kc.create_identity_provider(updated_idp, realm) kc.create_identity_provider(desired_idp, realm)
for mapper in mappers: for mapper in mappers:
if mapper.get('identityProviderAlias') is None: if mapper.get('identityProviderAlias') is None:
mapper['identityProviderAlias'] = alias mapper['identityProviderAlias'] = alias
@ -583,26 +582,28 @@ def main():
else: else:
if state == 'present': if state == 'present':
# Process an update
# no changes # no changes
if updated_idp == before_idp: if desired_idp == before_idp:
result['changed'] = False result['changed'] = False
result['end_state'] = sanitize(updated_idp) result['end_state'] = sanitize(desired_idp)
result['msg'] = "No changes required to identity provider {alias}.".format(alias=alias) result['msg'] = "No changes required to identity provider {alias}.".format(alias=alias)
module.exit_json(**result) module.exit_json(**result)
# update the existing role # doing an update
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize(before_idp), after=sanitize(updated_idp)) result['diff'] = dict(before=sanitize(before_idp), after=sanitize(desired_idp))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do the update # do the update
updated_idp = updated_idp.copy() desired_idp = desired_idp.copy()
updated_mappers = updated_idp.pop('mappers', []) updated_mappers = desired_idp.pop('mappers', [])
kc.update_identity_provider(updated_idp, realm) kc.update_identity_provider(desired_idp, realm)
for mapper in updated_mappers: for mapper in updated_mappers:
if mapper.get('id') is not None: if mapper.get('id') is not None:
kc.update_identity_provider_mapper(mapper, alias, realm) kc.update_identity_provider_mapper(mapper, alias, realm)
@ -622,6 +623,7 @@ def main():
module.exit_json(**result) module.exit_json(**result)
elif state == 'absent': elif state == 'absent':
# Process a deletion
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
@ -630,13 +632,12 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete for real # delete it
kc.delete_identity_provider(alias, realm) kc.delete_identity_provider(alias, realm)
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = "Identity provider {alias} has been deleted".format(alias=alias) result['msg'] = "Identity provider {alias} has been deleted".format(alias=alias)
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)

View file

@ -13,6 +13,7 @@ DOCUMENTATION = '''
module: keycloak_realm module: keycloak_realm
short_description: Allows administration of Keycloak realm via Keycloak API short_description: Allows administration of Keycloak realm via Keycloak API
version_added: 3.0.0 version_added: 3.0.0
@ -533,20 +534,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Realm testrealm has been updated" sample: "Realm testrealm has been updated"
proposed: proposed:
description: realm representation of proposed changes to realm description: Representation of proposed realm.
returned: always returned: always
type: dict type: dict
sample: { sample: {
id: "test" id: "test"
} }
existing: existing:
description: realm representation of existing realm (sample is truncated) description: Representation of existing realm (sample is truncated).
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -555,9 +557,10 @@ existing:
"request.object.signature.alg": "RS256", "request.object.signature.alg": "RS256",
} }
} }
end_state: end_state:
description: realm representation of realm after module execution (sample is truncated) description: Representation of realm after module execution (sample is truncated).
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"adminUrl": "http://www.example.com/admin_url", "adminUrl": "http://www.example.com/admin_url",
@ -573,7 +576,7 @@ from ansible.module_utils.basic import AnsibleModule
def sanitize_cr(realmrep): def sanitize_cr(realmrep):
""" Removes probably sensitive details from a realm representation """ Removes probably sensitive details from a realm representation.
:param realmrep: the realmrep dict to be sanitized :param realmrep: the realmrep dict to be sanitized
:return: sanitized realmrep dict :return: sanitized realmrep dict
@ -676,6 +679,7 @@ def main():
verify_email=dict(type='bool', aliases=['verifyEmail']), verify_email=dict(type='bool', aliases=['verifyEmail']),
wait_increment_seconds=dict(type='int', aliases=['waitIncrementSeconds']), wait_increment_seconds=dict(type='int', aliases=['waitIncrementSeconds']),
) )
argument_spec.update(meta_args) argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
@ -699,95 +703,114 @@ def main():
# convert module parameters to realm representation parameters (if they belong in there) # convert module parameters to realm representation parameters (if they belong in there)
params_to_ignore = list(keycloak_argument_spec().keys()) + ['state'] params_to_ignore = list(keycloak_argument_spec().keys()) + ['state']
# Filter and map the parameters names that apply to the role
realm_params = [x for x in module.params realm_params = [x for x in module.params
if x not in params_to_ignore and if x not in params_to_ignore and
module.params.get(x) is not None] module.params.get(x) is not None]
# See whether the realm already exists in Keycloak # See whether the realm already exists in Keycloak
before_realm = kc.get_realm_by_id(realm=realm) or {} before_realm = kc.get_realm_by_id(realm=realm)
if before_realm is None:
before_realm = {}
# Build a proposed changeset from parameters given to this module # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for realm_param in realm_params: for realm_param in realm_params:
new_param_value = module.params.get(realm_param) new_param_value = module.params.get(realm_param)
changeset[camel(realm_param)] = new_param_value changeset[camel(realm_param)] = new_param_value
# Whether creating or updating a realm, take the before-state and merge the changeset into it # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_realm = before_realm.copy() desired_realm = before_realm.copy()
updated_realm.update(changeset) desired_realm.update(changeset)
result['proposed'] = sanitize_cr(changeset) result['proposed'] = sanitize_cr(changeset)
before_realm_sanitized = sanitize_cr(before_realm) before_realm_sanitized = sanitize_cr(before_realm)
result['existing'] = before_realm_sanitized result['existing'] = before_realm_sanitized
# If the realm does not exist yet, before_realm is still empty # Cater for when it doesn't exist (an empty dict)
if not before_realm: if not before_realm:
if state == 'absent': if state == 'absent':
# do nothing and exit # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = {}
result['msg'] = 'Realm does not exist, doing nothing.' result['msg'] = 'Realm does not exist, doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# create new realm # Process a creation
result['changed'] = True result['changed'] = True
if 'id' not in updated_realm:
if 'id' not in desired_realm:
module.fail_json(msg='id needs to be specified when creating a new realm') module.fail_json(msg='id needs to be specified when creating a new realm')
if module._diff: if module._diff:
result['diff'] = dict(before='', after=sanitize_cr(updated_realm)) result['diff'] = dict(before='', after=sanitize_cr(desired_realm))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
kc.create_realm(updated_realm) # create it
after_realm = kc.get_realm_by_id(updated_realm['id']) kc.create_realm(desired_realm)
after_realm = kc.get_realm_by_id(desired_realm['id'])
result['end_state'] = sanitize_cr(after_realm) result['end_state'] = sanitize_cr(after_realm)
result['msg'] = 'Realm %s has been created.' % updated_realm['id'] result['msg'] = 'Realm %s has been created.' % desired_realm['id']
module.exit_json(**result) module.exit_json(**result)
else: else:
if state == 'present': if state == 'present':
# update existing realm # Process an update
# doing an update
result['changed'] = True result['changed'] = True
if module.check_mode: if module.check_mode:
# We can only compare the current realm with the proposed updates we have # We can only compare the current realm with the proposed updates we have
if module._diff: if module._diff:
result['diff'] = dict(before=before_realm_sanitized, result['diff'] = dict(before=before_realm_sanitized,
after=sanitize_cr(updated_realm)) after=sanitize_cr(desired_realm))
result['changed'] = (before_realm != updated_realm) result['changed'] = (before_realm != desired_realm)
module.exit_json(**result) module.exit_json(**result)
kc.update_realm(updated_realm, realm=realm) # do the update
kc.update_realm(desired_realm, realm=realm)
after_realm = kc.get_realm_by_id(realm=realm) after_realm = kc.get_realm_by_id(realm=realm)
if before_realm == after_realm: if before_realm == after_realm:
result['changed'] = False result['changed'] = False
result['end_state'] = sanitize_cr(after_realm)
if module._diff: if module._diff:
result['diff'] = dict(before=before_realm_sanitized, result['diff'] = dict(before=before_realm_sanitized,
after=sanitize_cr(after_realm)) after=sanitize_cr(after_realm))
result['end_state'] = sanitize_cr(after_realm)
result['msg'] = 'Realm %s has been updated.' % updated_realm['id'] result['msg'] = 'Realm %s has been updated.' % desired_realm['id']
module.exit_json(**result) module.exit_json(**result)
else: else:
# Delete existing realm # Process a deletion (because state was not 'present')
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff']['before'] = before_realm_sanitized result['diff'] = dict(before=before_realm_sanitized, after='')
result['diff']['after'] = ''
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete it
kc.delete_realm(realm=realm) kc.delete_realm(realm=realm)
result['proposed'] = dict()
result['end_state'] = dict() result['proposed'] = {}
result['end_state'] = {}
result['msg'] = 'Realm %s has been deleted.' % before_realm['id'] result['msg'] = 'Realm %s has been deleted.' % before_realm['id']
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)

View file

@ -149,20 +149,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
msg: msg:
description: Message as to what action was taken description: Message as to what action was taken.
returned: always returned: always
type: str type: str
sample: "Role myrole has been updated" sample: "Role myrole has been updated"
proposed: proposed:
description: Role representation of proposed changes to role description: Representation of proposed role.
returned: always returned: always
type: dict type: dict
sample: { sample: {
"description": "My updated test description" "description": "My updated test description"
} }
existing: existing:
description: Role representation of existing role description: Representation of existing role.
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -174,9 +175,10 @@ existing:
"id": "561703dd-0f38-45ff-9a5a-0c978f794547", "id": "561703dd-0f38-45ff-9a5a-0c978f794547",
"name": "myrole" "name": "myrole"
} }
end_state: end_state:
description: Role representation of role after module execution (sample is truncated) description: Representation of role after module execution (sample is truncated).
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"attributes": {}, "attributes": {},
@ -201,6 +203,7 @@ def main():
:return: :return:
""" """
argument_spec = keycloak_argument_spec() argument_spec = keycloak_argument_spec()
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),
@ -239,22 +242,22 @@ def main():
for key, val in module.params['attributes'].items(): for key, val in module.params['attributes'].items():
module.params['attributes'][key] = [val] if not isinstance(val, list) else val module.params['attributes'][key] = [val] if not isinstance(val, list) else val
# convert module parameters to client representation parameters (if they belong in there) # 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', 'composites'] and
module.params.get(x) is not None] module.params.get(x) is not None]
# does the role already exist? # See if it already exists in Keycloak
if clientid is None: if clientid is None:
before_role = kc.get_realm_role(name, realm) before_role = kc.get_realm_role(name, realm)
else: else:
before_role = kc.get_client_role(name, clientid, realm) before_role = kc.get_client_role(name, clientid, realm)
if before_role is None: if before_role is None:
before_role = dict() before_role = {}
# build a changeset # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for param in role_params: for param in role_params:
new_param_value = module.params.get(param) new_param_value = module.params.get(param)
@ -262,42 +265,42 @@ def main():
if new_param_value != old_value: if new_param_value != old_value:
changeset[camel(param)] = new_param_value changeset[camel(param)] = new_param_value
# prepare the new role # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_role = before_role.copy() desired_role = before_role.copy()
updated_role.update(changeset) desired_role.update(changeset)
result['proposed'] = changeset result['proposed'] = changeset
result['existing'] = before_role result['existing'] = before_role
# if before_role is none, the role doesn't exist. # Cater for when it doesn't exist (an empty dict)
if before_role == dict(): if not before_role:
if state == 'absent': if state == 'absent':
# nothing to do. # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False result['changed'] = False
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = 'Role does not exist; doing nothing.' result['msg'] = 'Role does not exist, doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# for 'present', create a new role. # Process a creation
result['changed'] = True result['changed'] = True
if name is None: if name is None:
module.fail_json(msg='name must be specified when creating a new role') module.fail_json(msg='name must be specified when creating a new role')
if module._diff: if module._diff:
result['diff'] = dict(before='', after=updated_role) result['diff'] = dict(before='', after=desired_role)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do it for real! # create it
if clientid is None: if clientid is None:
kc.create_realm_role(updated_role, realm) kc.create_realm_role(desired_role, realm)
after_role = kc.get_realm_role(name, realm) after_role = kc.get_realm_role(name, realm)
else: else:
kc.create_client_role(updated_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)
result['end_state'] = after_role result['end_state'] = after_role
@ -307,28 +310,30 @@ def main():
else: else:
if state == 'present': if state == 'present':
# Process an update
# no changes # no changes
if updated_role == before_role: if desired_role == before_role:
result['changed'] = False result['changed'] = False
result['end_state'] = updated_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)
module.exit_json(**result) module.exit_json(**result)
# update the existing role # doing an update
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=before_role, after=updated_role) result['diff'] = dict(before=before_role, after=desired_role)
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do the update # do the update
if clientid is None: if clientid is None:
kc.update_realm_role(updated_role, realm) kc.update_realm_role(desired_role, realm)
after_role = kc.get_realm_role(name, realm) after_role = kc.get_realm_role(name, realm)
else: else:
kc.update_client_role(updated_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)
result['end_state'] = after_role result['end_state'] = after_role
@ -336,7 +341,8 @@ def main():
result['msg'] = "Role {name} has been updated".format(name=name) result['msg'] = "Role {name} has been updated".format(name=name)
module.exit_json(**result) module.exit_json(**result)
elif state == 'absent': else:
# Process a deletion (because state was not 'present')
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
@ -345,16 +351,15 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete for real # delete it
if clientid is None: if clientid is None:
kc.delete_realm_role(name, realm) kc.delete_realm_role(name, realm)
else: else:
kc.delete_client_role(name, clientid, realm) kc.delete_client_role(name, clientid, realm)
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = "Role {name} has been deleted".format(name=name) result['msg'] = "Role {name} has been deleted".format(name=name)
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)

View file

@ -182,7 +182,7 @@ options:
description: description:
- For one level, the search applies only for users in the DNs specified by User DNs. - For one level, the search applies only for users in the DNs specified by User DNs.
For subtree, the search applies to the whole subtree. See LDAP documentation for For subtree, the search applies to the whole subtree. See LDAP documentation for
more details more details.
default: '1' default: '1'
type: str type: str
choices: choices:
@ -551,7 +551,7 @@ msg:
sample: "No changes required to user federation 164bb483-c613-482e-80fe-7f1431308799." sample: "No changes required to user federation 164bb483-c613-482e-80fe-7f1431308799."
proposed: proposed:
description: Representation of proposed changes to user federation. description: Representation of proposed user federation.
returned: always returned: always
type: dict type: dict
sample: { sample: {
@ -648,7 +648,7 @@ existing:
end_state: end_state:
description: Representation of user federation after module execution. description: Representation of user federation after module execution.
returned: always returned: on success
type: dict type: dict
sample: { sample: {
"config": { "config": {
@ -668,7 +668,6 @@ end_state:
"providerId": "kerberos", "providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider" "providerType": "org.keycloak.storage.UserStorageProvider"
} }
''' '''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
@ -808,12 +807,12 @@ def main():
mapper['config'] = dict((k, [str(v).lower() if not isinstance(v, str) else v]) mapper['config'] = dict((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) for k, v in mapper['config'].items() if mapper['config'][k] is not None)
# convert module parameters to client representation parameters (if they belong in there) # 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()) + ['state', 'realm', 'mappers'] and
module.params.get(x) is not None] module.params.get(x) is not None]
# does the user federation already exist? # See if it already exists in Keycloak
if cid is None: if cid is None:
found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', parent=realm, name=name)), realm) found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', parent=realm, name=name)), realm)
if len(found) > 1: if len(found) > 1:
@ -825,14 +824,14 @@ def main():
before_comp = kc.get_component(cid, realm) before_comp = kc.get_component(cid, realm)
if before_comp is None: if before_comp is None:
before_comp = dict() before_comp = {}
# if user federation exists, get associated mappers # if user federation exists, get associated mappers
if cid is not None: if cid is not None:
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'))
# build a changeset # Build a proposed changeset from parameters given to this module
changeset = dict() changeset = {}
for param in comp_params: for param in comp_params:
new_param_value = module.params.get(param) new_param_value = module.params.get(param)
@ -851,11 +850,11 @@ def main():
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 = dict() 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 = kc.get_component(change['id'], realm)
if old_mapper is None: if old_mapper is None:
old_mapper = dict() old_mapper = {}
else: else:
found = kc.get_components(urlencode(dict(parent=cid, name=change['name'])), realm) found = kc.get_components(urlencode(dict(parent=cid, name=change['name'])), realm)
if len(found) > 1: if len(found) > 1:
@ -863,7 +862,7 @@ def main():
if len(found) == 1: if len(found) == 1:
old_mapper = found[0] old_mapper = found[0]
else: else:
old_mapper = dict() 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: if new_mapper != old_mapper:
@ -871,37 +870,37 @@ def main():
changeset['mappers'] = list() changeset['mappers'] = list()
changeset['mappers'].append(new_mapper) changeset['mappers'].append(new_mapper)
# prepare the new representation # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
updated_comp = before_comp.copy() desired_comp = before_comp.copy()
updated_comp.update(changeset) desired_comp.update(changeset)
result['proposed'] = sanitize(changeset) result['proposed'] = sanitize(changeset)
result['existing'] = sanitize(before_comp) result['existing'] = sanitize(before_comp)
# if before_comp is none, the user federation doesn't exist. # Cater for when it doesn't exist (an empty dict)
if before_comp == dict(): if not before_comp:
if state == 'absent': if state == 'absent':
# nothing to do. # Do nothing and exit
if module._diff: if module._diff:
result['diff'] = dict(before='', after='') result['diff'] = dict(before='', after='')
result['changed'] = False result['changed'] = False
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = 'User federation does not exist; doing nothing.' result['msg'] = 'User federation does not exist; doing nothing.'
module.exit_json(**result) module.exit_json(**result)
# for 'present', create a new user federation. # Process a creation
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before='', after=sanitize(updated_comp)) result['diff'] = dict(before='', after=sanitize(desired_comp))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do it for real! # create it
updated_comp = updated_comp.copy() desired_comp = desired_comp.copy()
updated_mappers = updated_comp.pop('mappers', []) updated_mappers = desired_comp.pop('mappers', [])
after_comp = kc.create_component(updated_comp, realm) after_comp = kc.create_component(desired_comp, realm)
for mapper in updated_mappers: for mapper in updated_mappers:
if mapper.get('id') is not None: if mapper.get('id') is not None:
@ -919,26 +918,28 @@ def main():
else: else:
if state == 'present': if state == 'present':
# Process an update
# no changes # no changes
if updated_comp == before_comp: if desired_comp == before_comp:
result['changed'] = False result['changed'] = False
result['end_state'] = sanitize(updated_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)
module.exit_json(**result) module.exit_json(**result)
# update the existing role # doing an update
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
result['diff'] = dict(before=sanitize(before_comp), after=sanitize(updated_comp)) result['diff'] = dict(before=sanitize(before_comp), after=sanitize(desired_comp))
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# do the update # do the update
updated_comp = updated_comp.copy() desired_comp = desired_comp.copy()
updated_mappers = updated_comp.pop('mappers', []) updated_mappers = desired_comp.pop('mappers', [])
kc.update_component(updated_comp, realm) kc.update_component(desired_comp, realm)
after_comp = kc.get_component(cid, realm) after_comp = kc.get_component(cid, realm)
for mapper in updated_mappers: for mapper in updated_mappers:
@ -946,7 +947,7 @@ def main():
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'] = updated_comp['id'] mapper['parentId'] = desired_comp['id']
mapper = kc.create_component(mapper, realm) mapper = kc.create_component(mapper, realm)
after_comp['mappers'] = updated_mappers after_comp['mappers'] = updated_mappers
@ -956,6 +957,7 @@ def main():
module.exit_json(**result) module.exit_json(**result)
elif state == 'absent': elif state == 'absent':
# Process a deletion
result['changed'] = True result['changed'] = True
if module._diff: if module._diff:
@ -964,13 +966,12 @@ def main():
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
# delete for real # delete it
kc.delete_component(cid, realm) kc.delete_component(cid, realm)
result['end_state'] = dict() result['end_state'] = {}
result['msg'] = "User federation {id} has been deleted".format(id=cid) result['msg'] = "User federation {id} has been deleted".format(id=cid)
module.exit_json(**result)
module.exit_json(**result) module.exit_json(**result)