From 996dc617edacfde398eae1067dbd04af3de42265 Mon Sep 17 00:00:00 2001 From: Pierre Dumuid Date: Fri, 22 Oct 2021 16:27:18 +1030 Subject: [PATCH] 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 Co-authored-by: Felix Fontein --- ...eycloak-module-cleanup-and-consistency.yml | 6 + .../keycloak/keycloak_authentication.py | 133 +++++++++++++-- .../identity/keycloak/keycloak_client.py | 101 ++++++----- .../keycloak/keycloak_client_rolemapping.py | 19 ++- .../identity/keycloak/keycloak_clientscope.py | 99 ++++++----- .../keycloak/keycloak_clienttemplate.py | 106 +++++++----- .../identity/keycloak/keycloak_group.py | 159 +++++++++++++----- .../keycloak/keycloak_identity_provider.py | 75 +++++---- .../identity/keycloak/keycloak_realm.py | 91 ++++++---- .../identity/keycloak/keycloak_role.py | 77 +++++---- .../keycloak/keycloak_user_federation.py | 73 ++++---- 11 files changed, 593 insertions(+), 346 deletions(-) create mode 100644 changelogs/fragments/3280-keycloak-module-cleanup-and-consistency.yml diff --git a/changelogs/fragments/3280-keycloak-module-cleanup-and-consistency.yml b/changelogs/fragments/3280-keycloak-module-cleanup-and-consistency.yml new file mode 100644 index 0000000000..4d06070886 --- /dev/null +++ b/changelogs/fragments/3280-keycloak-module-cleanup-and-consistency.yml @@ -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). diff --git a/plugins/modules/identity/keycloak/keycloak_authentication.py b/plugins/modules/identity/keycloak/keycloak_authentication.py index 5a48eab2ce..c7bf5bc01f 100644 --- a/plugins/modules/identity/keycloak/keycloak_authentication.py +++ b/plugins/modules/identity/keycloak/keycloak_authentication.py @@ -9,11 +9,15 @@ __metaclass__ = type DOCUMENTATION = ''' --- module: keycloak_authentication + short_description: Configure authentication in Keycloak + description: - 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. + version_added: "3.3.0" + options: realm: description: @@ -79,6 +83,7 @@ options: default: false description: - If C(true), allows to remove the authentication flow and recreate it. + extends_documentation_fragment: - community.general.keycloak @@ -162,10 +167,74 @@ EXAMPLES = ''' ''' RETURN = ''' +msg: + description: Message as to what action was taken. + returned: always + type: str + flow: - description: JSON representation for the authentication. - returned: on success - type: dict + description: + - JSON representation for the authentication. + - 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 \ @@ -271,9 +340,11 @@ def create_or_update_executions(kc, config, realm='master'): def main(): """ Module execution + :return: """ argument_spec = keycloak_argument_spec() + meta_args = dict( realm=dict(type='str', required=True), alias=dict(type='str', required=True), @@ -292,6 +363,7 @@ def main(): state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default=False), ) + argument_spec.update(meta_args) module = AnsibleModule(argument_spec=argument_spec, @@ -301,6 +373,7 @@ def main(): ) result = dict(changed=False, msg='', flow={}) + # Obtain access token, initialize API try: connection_header = get_token(module.params) @@ -308,6 +381,7 @@ def main(): module.fail_json(msg=str(e)) kc = KeycloakAPI(module, connection_header) + realm = module.params.get('realm') state = module.params.get('state') 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) - 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 + if module._diff: result['diff'] = dict(before='', after=new_auth_repr) + if module.check_mode: module.exit_json(**result) + # If copyFrom is defined, create authentication flow from a copy 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) else: # Create an empty authentication flow 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 auth_repr is None: result['msg'] = "Authentication just created not found: " + str(new_auth_repr) module.fail_json(**result) + # Configure the executions for the flow create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm) + # Get executions created exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm) if exec_repr is not None: auth_repr["authenticationExecutions"] = exec_repr - result['flow'] = auth_repr - elif state == 'absent': # If desired state is absent. - if module._diff: - result['diff'] = dict(before='', after='') - result['msg'] = new_auth_repr["alias"] + ' absent' - else: # The authentication flow already exist - if state == 'present': # if desired state is present + result['end_state'] = auth_repr + result['flow'] = result['end_state'] + + else: + if state == 'present': + # Process an update + if force: # If force option is true # Delete the actual authentication flow result['changed'] = True @@ -370,25 +463,35 @@ def main(): result['msg'] = "Authentication just created not found: " + str(new_auth_repr) module.fail_json(**result) # Configure the executions for the flow + if module.check_mode: module.exit_json(**result) changed, diff = create_or_update_executions(kc=kc, config=new_auth_repr, realm=realm) result['changed'] |= changed + if module._diff: result['diff'] = diff + # Get executions created exec_repr = kc.get_executions_representation(config=new_auth_repr, realm=realm) if exec_repr is not None: auth_repr["authenticationExecutions"] = exec_repr - result['flow'] = auth_repr - elif state == 'absent': # If desired state is absent + result['end_state'] = auth_repr + result['flow'] = result['end_state'] + + else: + # Process a deletion (because state was not 'present') result['changed'] = True - # Delete the authentication flow alias. + if module._diff: result['diff'] = dict(before=auth_repr, after='') + if module.check_mode: module.exit_json(**result) + + # delete it 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'], id=auth_repr["id"]) diff --git a/plugins/modules/identity/keycloak/keycloak_client.py b/plugins/modules/identity/keycloak/keycloak_client.py index e37997e752..4309918f79 100644 --- a/plugins/modules/identity/keycloak/keycloak_client.py +++ b/plugins/modules/identity/keycloak/keycloak_client.py @@ -62,17 +62,17 @@ options: name: description: - - Name of the client (this is not the same as I(client_id)) + - Name of the client (this is not the same as I(client_id)). type: str description: description: - - Description of the client in Keycloak + - Description of the client in Keycloak. type: str root_url: 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. aliases: - rootUrl @@ -80,7 +80,7 @@ options: admin_url: 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. aliases: - adminUrl @@ -357,7 +357,7 @@ options: protocol: 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. choices: ['openid-connect', 'saml'] type: str @@ -513,7 +513,6 @@ options: extends_documentation_fragment: - community.general.keycloak - author: - Eike Frost (@eikef) ''' @@ -645,20 +644,21 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Client testclient has been updated" + description: Message as to what action was taken. + returned: always + type: str + sample: "Client testclient has been updated" proposed: - description: client representation of proposed changes to client + description: Representation of proposed client. returned: always type: dict sample: { clientId: "test" } + existing: - description: client representation of existing client (sample is truncated) + description: Representation of existing client (sample is truncated). returned: always type: dict sample: { @@ -667,9 +667,10 @@ existing: "request.object.signature.alg": "RS256", } } + end_state: - description: client representation of client after module execution (sample is truncated) - returned: always + description: Representation of client after module execution (sample is truncated). + returned: on success type: dict sample: { "adminUrl": "http://www.example.com/admin_url", @@ -685,7 +686,7 @@ from ansible.module_utils.basic import AnsibleModule 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 :return: sanitized clientrep dict @@ -759,6 +760,7 @@ def main(): protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']), authorization_settings=dict(type='dict', aliases=['authorizationSettings']), ) + argument_spec.update(meta_args) module = AnsibleModule(argument_spec=argument_spec, @@ -781,12 +783,12 @@ def main(): cid = module.params.get('id') 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 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and 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: before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm) if before_client is not None: @@ -795,10 +797,10 @@ def main(): before_client = kc.get_client_by_id(cid, realm=realm) if before_client is None: - before_client = dict() + before_client = {} # Build a proposed changeset from parameters given to this module - changeset = dict() + changeset = {} for client_param in client_params: new_param_value = module.params.get(client_param) @@ -817,54 +819,61 @@ def main(): changeset[camel(client_param)] = new_param_value - # Whether creating or updating a client, take the before-state and merge the changeset into it - updated_client = before_client.copy() - updated_client.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_client = before_client.copy() + desired_client.update(changeset) result['proposed'] = sanitize_cr(changeset) result['existing'] = sanitize_cr(before_client) - # If the client does not exist yet, before_client is still empty - if before_client == dict(): + # Cater for when it doesn't exist (an empty dict) + if not before_client: if state == 'absent': - # do nothing and exit + # Do nothing and exit if module._diff: 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) - # create new client + # Process a creation 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') 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: module.exit_json(**result) - kc.create_client(updated_client, realm=realm) - after_client = kc.get_client_by_clientid(updated_client['clientId'], realm=realm) + # create it + 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['msg'] = 'Client %s has been created.' % updated_client['clientId'] + result['msg'] = 'Client %s has been created.' % desired_client['clientId'] module.exit_json(**result) + else: if state == 'present': - # update existing client + # Process an update result['changed'] = True + if module.check_mode: # We can only compare the current client with the proposed updates we have if module._diff: result['diff'] = dict(before=sanitize_cr(before_client), - after=sanitize_cr(updated_client)) - result['changed'] = (before_client != updated_client) + after=sanitize_cr(desired_client)) + result['changed'] = (before_client != desired_client) 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) if before_client == after_client: @@ -872,25 +881,29 @@ def main(): if module._diff: result['diff'] = dict(before=sanitize_cr(before_client), after=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) + else: - # Delete existing client + # Process a deletion (because state was not 'present') result['changed'] = True + if module._diff: - result['diff']['before'] = sanitize_cr(before_client) - result['diff']['after'] = '' + result['diff'] = dict(before=sanitize_cr(before_client), after='') if module.check_mode: module.exit_json(**result) + # delete it kc.delete_client(cid, realm=realm) - result['proposed'] = dict() - result['end_state'] = dict() + result['proposed'] = {} + + result['end_state'] = {} + result['msg'] = 'Client %s has been deleted.' % before_client['clientId'] - module.exit_json(**result) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py b/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py index e3d43d7919..b7cd70c122 100644 --- a/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py +++ b/plugins/modules/identity/keycloak/keycloak_client_rolemapping.py @@ -11,6 +11,7 @@ DOCUMENTATION = ''' module: keycloak_client_rolemapping short_description: Allows administration of Keycloak client_rolemapping with the Keycloak API + version_added: 3.5.0 description: @@ -158,21 +159,22 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Role role1 assigned to group group1." + description: Message as to what action was taken. + returned: always + type: str + sample: "Role role1 assigned to group group1." proposed: - description: role_representation representation of proposed changes to client_rolemapping. + description: Representation of proposed client role mapping. returned: always type: dict sample: { clientId: "test" } + existing: description: - - role_representation representation of existing role_representation. + - Representation of existing client role mapping. - The sample is truncated. returned: always type: dict @@ -182,11 +184,12 @@ existing: "request.object.signature.alg": "RS256", } } + end_state: description: - - role_representation representation of role_representation after module execution. + - Representation of client role mapping after module execution. - The sample is truncated. - returned: always + returned: on success type: dict sample: { "adminUrl": "http://www.example.com/admin_url", diff --git a/plugins/modules/identity/keycloak/keycloak_clientscope.py b/plugins/modules/identity/keycloak/keycloak_clientscope.py index c05050aae5..2deab5547d 100644 --- a/plugins/modules/identity/keycloak/keycloak_clientscope.py +++ b/plugins/modules/identity/keycloak/keycloak_clientscope.py @@ -86,7 +86,7 @@ options: suboptions: protocol: description: - - This specifies for which protocol this protocol mapper + - This specifies for which protocol this protocol mapper. - is active. choices: ['openid-connect', 'saml', 'wsfed'] type: str @@ -256,20 +256,21 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Client_scope testclientscope has been updated" + description: Message as to what action was taken. + returned: always + type: str + sample: "Client_scope testclientscope has been updated" proposed: - description: client_scope representation of proposed changes to client_scope + description: Representation of proposed client scope. returned: always type: dict sample: { clientId: "test" } + existing: - description: client_scope representation of existing client_scope (sample is truncated) + description: Representation of existing client scope (sample is truncated). returned: always type: dict sample: { @@ -278,9 +279,10 @@ existing: "request.object.signature.alg": "RS256", } } + end_state: - description: client_scope representation of client_scope after module execution (sample is truncated) - returned: always + description: Representation of client scope after module execution (sample is truncated). + returned: on success type: dict sample: { "adminUrl": "http://www.example.com/admin_url", @@ -296,7 +298,7 @@ from ansible.module_utils.basic import AnsibleModule 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 :return: sanitized clientrep dict @@ -361,22 +363,22 @@ def main(): name = module.params.get('name') 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: before_clientscope = kc.get_clientscope_by_name(name, realm=realm) else: before_clientscope = kc.get_clientscope_by_clientscopeid(cid, realm=realm) - before_clientscope = {} if before_clientscope is None else 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] + if before_clientscope is None: + before_clientscope = {} # Build a proposed changeset from parameters given to this module - changeset = dict() + changeset = {} for clientscope_param in clientscope_params: 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] changeset[camel(clientscope_param)] = new_param_value - # prepare the new clientscope - updated_clientscope = before_clientscope.copy() - updated_clientscope.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_clientscope = before_clientscope.copy() + desired_clientscope.update(changeset) - # if before_clientscope is none, the clientscope doesn't exist. - if before_clientscope == {}: + # Cater for when it doesn't exist (an empty dict) + if not before_clientscope: if state == 'absent': - # nothing to do. + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') + result['changed'] = False + result['end_state'] = {} result['msg'] = 'Clientscope does not exist; doing nothing.' - result['end_state'] = dict() module.exit_json(**result) - # for 'present', create a new clientscope. + # Process a creation result['changed'] = True + if name is None: module.fail_json(msg='name must be specified when creating a new clientscope') 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: module.exit_json(**result) - # do it for real! - kc.create_clientscope(updated_clientscope, realm=realm) + # create it + kc.create_clientscope(desired_clientscope, realm=realm) after_clientscope = kc.get_clientscope_by_name(name, realm) result['end_state'] = sanitize_cr(after_clientscope) + result['msg'] = 'Clientscope {name} has been created with ID {id}'.format(name=after_clientscope['name'], id=after_clientscope['id']) else: if state == 'present': + # Process an update + # no changes - if updated_clientscope == before_clientscope: + if desired_clientscope == before_clientscope: 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']) module.exit_json(**result) - # update the existing clientscope + # doing an update result['changed'] = True 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: module.exit_json(**result) - # do the clientscope update - kc.update_clientscope(updated_clientscope, realm=realm) + # do the update + kc.update_clientscope(desired_clientscope, realm=realm) # do the protocolmappers update if protocol_mappers is not None: for protocol_mapper in protocol_mappers: # 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: 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 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['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) - elif state == 'absent': - result['end_state'] = dict() + else: + # Process a deletion (because state was not 'present') + result['changed'] = True if module._diff: result['diff'] = dict(before=sanitize_cr(before_clientscope), after='') @@ -476,14 +484,13 @@ def main(): if module.check_mode: module.exit_json(**result) - # delete for real + # delete it cid = before_clientscope['id'] kc.delete_clientscope(cid=cid, realm=realm) - result['changed'] = True - result['msg'] = "Clientscope {name} has been deleted".format(name=before_clientscope['name']) + result['end_state'] = {} - module.exit_json(**result) + result['msg'] = "Clientscope {name} has been deleted".format(name=before_clientscope['name']) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_clienttemplate.py b/plugins/modules/identity/keycloak/keycloak_clienttemplate.py index 82991aea85..cec7c93d8d 100644 --- a/plugins/modules/identity/keycloak/keycloak_clienttemplate.py +++ b/plugins/modules/identity/keycloak/keycloak_clienttemplate.py @@ -31,7 +31,7 @@ description: options: state: 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(absent), the client template will be removed if it exists choices: ['present', 'absent'] @@ -51,12 +51,12 @@ options: name: description: - - Name of the client template + - Name of the client template. type: str description: description: - - Description of the client template in Keycloak + - Description of the client template in Keycloak. type: str protocol: @@ -100,7 +100,7 @@ options: protocol: 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. choices: ['openid-connect', 'saml'] type: str @@ -143,7 +143,7 @@ options: 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 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 attributes: @@ -163,7 +163,6 @@ notes: extends_documentation_fragment: - community.general.keycloak - author: - Eike Frost (@eikef) ''' @@ -231,20 +230,21 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Client template testclient has been updated" + description: Message as to what action was taken. + returned: always + type: str + sample: "Client template testclient has been updated" proposed: - description: client template representation of proposed changes to client template + description: Representation of proposed client template. returned: always type: dict sample: { name: "test01" } + existing: - description: client template representation of existing client template (sample is truncated) + description: Representation of existing client template (sample is truncated). returned: always type: dict sample: { @@ -254,9 +254,10 @@ existing: "name": "test01", "protocol": "saml" } + end_state: - description: client template representation of client template after module execution (sample is truncated) - returned: always + description: Representation of client template after module execution (sample is truncated). + returned: on success type: dict sample: { "description": "test01", @@ -302,6 +303,7 @@ def main(): full_scope_allowed=dict(type='bool'), protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec), ) + argument_spec.update(meta_args) module = AnsibleModule(argument_spec=argument_spec, @@ -317,19 +319,20 @@ def main(): connection_header = get_token(module.params) except KeycloakError as e: module.fail_json(msg=str(e)) + kc = KeycloakAPI(module, connection_header) realm = module.params.get('realm') state = module.params.get('state') 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 if x not in ['state', 'auth_keycloak_url', 'auth_client_id', 'auth_realm', 'auth_client_secret', 'auth_username', 'auth_password', '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: before_clientt = kc.get_client_template_by_name(module.params.get('name'), realm=realm) if before_clientt is not None: @@ -338,12 +341,12 @@ def main(): before_clientt = kc.get_client_template_by_id(cid, realm=realm) if before_clientt is None: - before_clientt = dict() + before_clientt = {} result['existing'] = before_clientt # Build a proposed changeset from parameters given to this module - changeset = dict() + changeset = {} for clientt_param in clientt_params: # lists in the Keycloak API are sorted @@ -355,78 +358,89 @@ def main(): pass changeset[camel(clientt_param)] = new_param_value - # Whether creating or updating a client, take the before-state and merge the changeset into it - updated_clientt = before_clientt.copy() - updated_clientt.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_clientt = before_clientt.copy() + desired_clientt.update(changeset) result['proposed'] = changeset - # If the client template does not exist yet, before_client is still empty - if before_clientt == dict(): + # Cater for when it doesn't exist (an empty dict) + if not before_clientt: if state == 'absent': - # do nothing and exit + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') + result['changed'] = False + result['end_state'] = {} result['msg'] = 'Client template does not exist, doing nothing.' module.exit_json(**result) - # create new client template + # Process a creation 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') if module._diff: - result['diff'] = dict(before='', after=updated_clientt) + result['diff'] = dict(before='', after=desired_clientt) if module.check_mode: module.exit_json(**result) - kc.create_client_template(updated_clientt, realm=realm) - after_clientt = kc.get_client_template_by_name(updated_clientt['name'], realm=realm) + # create it + 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['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) + else: if state == 'present': - # update existing client template + # Process an update + result['changed'] = True if module.check_mode: # We can only compare the current client template with the proposed updates we have if module._diff: result['diff'] = dict(before=before_clientt, - after=updated_clientt) + after=desired_clientt) 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) if before_clientt == after_clientt: result['changed'] = False - if module._diff: - result['diff'] = dict(before=before_clientt, - after=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: - result['diff']['before'] = before_clientt - result['diff']['after'] = '' + result['diff'] = dict(before=before_clientt, after=after_clientt) + + 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: module.exit_json(**result) + # delete it kc.delete_client_template(cid, realm=realm) - result['proposed'] = dict() - result['end_state'] = dict() + result['proposed'] = {} + + result['end_state'] = {} + result['msg'] = 'Client template %s has been deleted.' % before_clientt['name'] - module.exit_json(**result) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_group.py b/plugins/modules/identity/keycloak/keycloak_group.py index 56e72fcb94..3455f57818 100644 --- a/plugins/modules/identity/keycloak/keycloak_group.py +++ b/plugins/modules/identity/keycloak/keycloak_group.py @@ -159,34 +159,91 @@ EXAMPLES = ''' ''' 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: - 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 type: complex contains: id: - description: GUID that identifies the group + description: GUID that identifies the group. type: str returned: always sample: 23f38145-3195-462c-97e7-97041ccea73e name: - description: Name of the group + description: Name of the group. type: str returned: always sample: grp-test-123 attributes: - description: Attributes applied to this group + description: Attributes applied to this group. type: dict returned: always sample: attr1: ["val1", "val2", "val3"] path: - description: URI path to the group + 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 + description: An array of the realm-level roles granted to this group. type: list returned: always sample: [] @@ -196,7 +253,7 @@ group: type: list returned: always 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 returned: always sample: [] @@ -208,6 +265,7 @@ group: manage: true manageMembership: true view: true + ''' from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ @@ -222,6 +280,7 @@ def main(): :return: """ argument_spec = keycloak_argument_spec() + meta_args = dict( state=dict(default='present', choices=['present', 'absent']), realm=dict(default='master'), @@ -254,16 +313,6 @@ def main(): name = module.params.get('name') 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 # via the API. attributes is a dict, so we'll transparently convert # the values to lists. @@ -271,81 +320,103 @@ def main(): for key, val in module.params['attributes'].items(): 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 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm'] and 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 = {} + for param in group_params: new_param_value = module.params.get(param) old_value = before_group[param] if param in before_group else None if new_param_value != old_value: changeset[camel(param)] = new_param_value - # prepare the new group - updated_group = before_group.copy() - updated_group.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_group = before_group.copy() + desired_group.update(changeset) - # if before_group is none, the group doesn't exist. - if before_group == {}: + # Cater for when it doesn't exist (an empty dict) + if not before_group: if state == 'absent': - # nothing to do. + # Do nothing and exit if module._diff: 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['group'] = dict() module.exit_json(**result) - # for 'present', create a new group. + # Process a creation result['changed'] = True + if name is None: module.fail_json(msg='name must be specified when creating a new group') if module._diff: - result['diff'] = dict(before='', after=updated_group) + result['diff'] = dict(before='', after=desired_group) if module.check_mode: module.exit_json(**result) - # do it for real! - kc.create_group(updated_group, realm=realm) + # create it + kc.create_group(desired_group, realm=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'], id=after_group['id']) + module.exit_json(**result) else: if state == 'present': + # Process an update + # no changes - if updated_group == before_group: + if desired_group == before_group: 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']) module.exit_json(**result) - # update the existing group + # doing an update result['changed'] = True 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: module.exit_json(**result) # 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']) - module.exit_json(**result) - elif state == 'absent': - result['group'] = dict() + else: + # Process a deletion (because state was not 'present') + result['changed'] = True if module._diff: result['diff'] = dict(before=before_group, after='') @@ -353,14 +424,14 @@ def main(): if module.check_mode: module.exit_json(**result) - # delete for real + # delete it gid = before_group['id'] kc.delete_group(groupid=gid, realm=realm) - result['changed'] = True - result['msg'] = "Group {name} has been deleted".format(name=before_group['name']) + result['end_state'] = {} + 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) diff --git a/plugins/modules/identity/keycloak/keycloak_identity_provider.py b/plugins/modules/identity/keycloak/keycloak_identity_provider.py index f5ccd899c5..a4adddd951 100644 --- a/plugins/modules/identity/keycloak/keycloak_identity_provider.py +++ b/plugins/modules/identity/keycloak/keycloak_identity_provider.py @@ -339,13 +339,13 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Identity provider my-idp has been created" + description: Message as to what action was taken. + returned: always + type: str + sample: "Identity provider my-idp has been created" proposed: - description: Representation of proposed changes to identity provider + description: Representation of proposed identity provider. returned: always type: dict sample: { @@ -363,7 +363,7 @@ proposed: } existing: - description: Representation of existing identity provider + description: Representation of existing identity provider. returned: always type: dict sample: { @@ -391,8 +391,8 @@ existing: } end_state: - description: Representation of identity provider after module execution - returned: always + description: Representation of identity provider after module execution. + returned: on success type: dict sample: { "addReadTokenRoleOnCreate": false, @@ -416,7 +416,6 @@ end_state: "storeToken": false, "trustEmail": false, } - ''' 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: idp['mappers'] = sorted(kc.get_identity_provider_mappers(alias, realm), key=lambda x: x.get('name')) if idp is None: - idp = dict() + idp = {} return idp @@ -497,16 +496,16 @@ def main(): alias = module.params.get('alias') 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 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'mappers'] and 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) - # build a changeset - changeset = dict() + # Build a proposed changeset from parameters given to this module + changeset = {} for param in idp_params: new_param_value = module.params.get(param) @@ -539,37 +538,37 @@ def main(): changeset['mappers'] = list() changeset['mappers'].append(new_mapper) - # prepare the new representation - updated_idp = before_idp.copy() - updated_idp.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_idp = before_idp.copy() + desired_idp.update(changeset) result['proposed'] = sanitize(changeset) result['existing'] = sanitize(before_idp) - # if before_idp is none, the identity provider doesn't exist. - if before_idp == dict(): + # Cater for when it doesn't exist (an empty dict) + if not before_idp: if state == 'absent': - # nothing to do. + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') result['changed'] = False - result['end_state'] = dict() + result['end_state'] = {} result['msg'] = 'Identity provider does not exist; doing nothing.' module.exit_json(**result) - # for 'present', create a new identity provider. + # Process a creation result['changed'] = True if module._diff: - result['diff'] = dict(before='', after=sanitize(updated_idp)) + result['diff'] = dict(before='', after=sanitize(desired_idp)) if module.check_mode: module.exit_json(**result) - # do it for real! - updated_idp = updated_idp.copy() - mappers = updated_idp.pop('mappers', []) - kc.create_identity_provider(updated_idp, realm) + # create it + desired_idp = desired_idp.copy() + mappers = desired_idp.pop('mappers', []) + kc.create_identity_provider(desired_idp, realm) for mapper in mappers: if mapper.get('identityProviderAlias') is None: mapper['identityProviderAlias'] = alias @@ -583,26 +582,28 @@ def main(): else: if state == 'present': + # Process an update + # no changes - if updated_idp == before_idp: + if desired_idp == before_idp: 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) module.exit_json(**result) - # update the existing role + # doing an update result['changed'] = True 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: module.exit_json(**result) # do the update - updated_idp = updated_idp.copy() - updated_mappers = updated_idp.pop('mappers', []) - kc.update_identity_provider(updated_idp, realm) + desired_idp = desired_idp.copy() + updated_mappers = desired_idp.pop('mappers', []) + kc.update_identity_provider(desired_idp, realm) for mapper in updated_mappers: if mapper.get('id') is not None: kc.update_identity_provider_mapper(mapper, alias, realm) @@ -622,6 +623,7 @@ def main(): module.exit_json(**result) elif state == 'absent': + # Process a deletion result['changed'] = True if module._diff: @@ -630,13 +632,12 @@ def main(): if module.check_mode: module.exit_json(**result) - # delete for real + # delete it kc.delete_identity_provider(alias, realm) - result['end_state'] = dict() + result['end_state'] = {} result['msg'] = "Identity provider {alias} has been deleted".format(alias=alias) - module.exit_json(**result) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_realm.py b/plugins/modules/identity/keycloak/keycloak_realm.py index da37fa2723..289c13504a 100644 --- a/plugins/modules/identity/keycloak/keycloak_realm.py +++ b/plugins/modules/identity/keycloak/keycloak_realm.py @@ -13,6 +13,7 @@ DOCUMENTATION = ''' module: keycloak_realm short_description: Allows administration of Keycloak realm via Keycloak API + version_added: 3.0.0 @@ -533,20 +534,21 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Realm testrealm has been updated" + description: Message as to what action was taken. + returned: always + type: str + sample: "Realm testrealm has been updated" proposed: - description: realm representation of proposed changes to realm + description: Representation of proposed realm. returned: always type: dict sample: { id: "test" } + existing: - description: realm representation of existing realm (sample is truncated) + description: Representation of existing realm (sample is truncated). returned: always type: dict sample: { @@ -555,9 +557,10 @@ existing: "request.object.signature.alg": "RS256", } } + end_state: - description: realm representation of realm after module execution (sample is truncated) - returned: always + description: Representation of realm after module execution (sample is truncated). + returned: on success type: dict sample: { "adminUrl": "http://www.example.com/admin_url", @@ -573,7 +576,7 @@ from ansible.module_utils.basic import AnsibleModule 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 :return: sanitized realmrep dict @@ -676,6 +679,7 @@ def main(): verify_email=dict(type='bool', aliases=['verifyEmail']), wait_increment_seconds=dict(type='int', aliases=['waitIncrementSeconds']), ) + argument_spec.update(meta_args) module = AnsibleModule(argument_spec=argument_spec, @@ -699,95 +703,114 @@ def main(): # convert module parameters to realm representation parameters (if they belong in there) 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 if x not in params_to_ignore and module.params.get(x) is not None] # 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 - changeset = dict() + changeset = {} for realm_param in realm_params: new_param_value = module.params.get(realm_param) changeset[camel(realm_param)] = new_param_value - # Whether creating or updating a realm, take the before-state and merge the changeset into it - updated_realm = before_realm.copy() - updated_realm.update(changeset) + # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) + desired_realm = before_realm.copy() + desired_realm.update(changeset) result['proposed'] = sanitize_cr(changeset) before_realm_sanitized = sanitize_cr(before_realm) 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 state == 'absent': - # do nothing and exit + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') + result['changed'] = False + result['end_state'] = {} result['msg'] = 'Realm does not exist, doing nothing.' module.exit_json(**result) - # create new realm + # Process a creation 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') 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: module.exit_json(**result) - kc.create_realm(updated_realm) - after_realm = kc.get_realm_by_id(updated_realm['id']) + # create it + kc.create_realm(desired_realm) + after_realm = kc.get_realm_by_id(desired_realm['id']) 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) + else: if state == 'present': - # update existing realm + # Process an update + + # doing an update result['changed'] = True if module.check_mode: # We can only compare the current realm with the proposed updates we have if module._diff: result['diff'] = dict(before=before_realm_sanitized, - after=sanitize_cr(updated_realm)) - result['changed'] = (before_realm != updated_realm) + after=sanitize_cr(desired_realm)) + result['changed'] = (before_realm != desired_realm) 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) + if before_realm == after_realm: result['changed'] = False + + result['end_state'] = sanitize_cr(after_realm) + if module._diff: result['diff'] = dict(before=before_realm_sanitized, 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) + else: - # Delete existing realm + # Process a deletion (because state was not 'present') result['changed'] = True + if module._diff: - result['diff']['before'] = before_realm_sanitized - result['diff']['after'] = '' + result['diff'] = dict(before=before_realm_sanitized, after='') if module.check_mode: module.exit_json(**result) + # delete it 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'] - module.exit_json(**result) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_role.py b/plugins/modules/identity/keycloak/keycloak_role.py index 23ed7cfeed..2dd2438e42 100644 --- a/plugins/modules/identity/keycloak/keycloak_role.py +++ b/plugins/modules/identity/keycloak/keycloak_role.py @@ -149,20 +149,21 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Message as to what action was taken - returned: always - type: str - sample: "Role myrole has been updated" + description: Message as to what action was taken. + returned: always + type: str + sample: "Role myrole has been updated" proposed: - description: Role representation of proposed changes to role + description: Representation of proposed role. returned: always type: dict sample: { "description": "My updated test description" } + existing: - description: Role representation of existing role + description: Representation of existing role. returned: always type: dict sample: { @@ -174,9 +175,10 @@ existing: "id": "561703dd-0f38-45ff-9a5a-0c978f794547", "name": "myrole" } + end_state: - description: Role representation of role after module execution (sample is truncated) - returned: always + description: Representation of role after module execution (sample is truncated). + returned: on success type: dict sample: { "attributes": {}, @@ -201,6 +203,7 @@ def main(): :return: """ argument_spec = keycloak_argument_spec() + meta_args = dict( state=dict(type='str', default='present', choices=['present', 'absent']), name=dict(type='str', required=True), @@ -239,22 +242,22 @@ def main(): for key, val in module.params['attributes'].items(): 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 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'client_id', 'composites'] and module.params.get(x) is not None] - # does the role already exist? + # See if it already exists in Keycloak if clientid is None: before_role = kc.get_realm_role(name, realm) else: before_role = kc.get_client_role(name, clientid, realm) if before_role is None: - before_role = dict() + before_role = {} - # build a changeset - changeset = dict() + # Build a proposed changeset from parameters given to this module + changeset = {} for param in role_params: new_param_value = module.params.get(param) @@ -262,42 +265,42 @@ def main(): if new_param_value != old_value: changeset[camel(param)] = new_param_value - # prepare the new role - updated_role = before_role.copy() - updated_role.update(changeset) + # 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.update(changeset) result['proposed'] = changeset result['existing'] = before_role - # if before_role is none, the role doesn't exist. - if before_role == dict(): + # Cater for when it doesn't exist (an empty dict) + if not before_role: if state == 'absent': - # nothing to do. + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') result['changed'] = False - result['end_state'] = dict() - result['msg'] = 'Role does not exist; doing nothing.' + result['end_state'] = {} + result['msg'] = 'Role does not exist, doing nothing.' module.exit_json(**result) - # for 'present', create a new role. + # Process a creation result['changed'] = True if name is None: module.fail_json(msg='name must be specified when creating a new role') if module._diff: - result['diff'] = dict(before='', after=updated_role) + result['diff'] = dict(before='', after=desired_role) if module.check_mode: module.exit_json(**result) - # do it for real! + # create it 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) 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) result['end_state'] = after_role @@ -307,28 +310,30 @@ def main(): else: if state == 'present': + # Process an update + # no changes - if updated_role == before_role: + if desired_role == before_role: result['changed'] = False - result['end_state'] = updated_role + result['end_state'] = desired_role result['msg'] = "No changes required to role {name}.".format(name=name) module.exit_json(**result) - # update the existing role + # doing an update result['changed'] = True 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: module.exit_json(**result) # do the update 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) 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) result['end_state'] = after_role @@ -336,7 +341,8 @@ def main(): result['msg'] = "Role {name} has been updated".format(name=name) module.exit_json(**result) - elif state == 'absent': + else: + # Process a deletion (because state was not 'present') result['changed'] = True if module._diff: @@ -345,16 +351,15 @@ def main(): if module.check_mode: module.exit_json(**result) - # delete for real + # delete it if clientid is None: kc.delete_realm_role(name, realm) else: kc.delete_client_role(name, clientid, realm) - result['end_state'] = dict() + result['end_state'] = {} result['msg'] = "Role {name} has been deleted".format(name=name) - module.exit_json(**result) module.exit_json(**result) diff --git a/plugins/modules/identity/keycloak/keycloak_user_federation.py b/plugins/modules/identity/keycloak/keycloak_user_federation.py index 4298334924..7c4b0e41c9 100644 --- a/plugins/modules/identity/keycloak/keycloak_user_federation.py +++ b/plugins/modules/identity/keycloak/keycloak_user_federation.py @@ -182,7 +182,7 @@ options: description: - 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 - more details + more details. default: '1' type: str choices: @@ -551,7 +551,7 @@ msg: sample: "No changes required to user federation 164bb483-c613-482e-80fe-7f1431308799." proposed: - description: Representation of proposed changes to user federation. + description: Representation of proposed user federation. returned: always type: dict sample: { @@ -648,7 +648,7 @@ existing: end_state: description: Representation of user federation after module execution. - returned: always + returned: on success type: dict sample: { "config": { @@ -668,7 +668,6 @@ end_state: "providerId": "kerberos", "providerType": "org.keycloak.storage.UserStorageProvider" } - ''' 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]) 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 if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'mappers'] and module.params.get(x) is not None] - # does the user federation already exist? + # See if it already exists in Keycloak if cid is None: found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', parent=realm, name=name)), realm) if len(found) > 1: @@ -825,14 +824,14 @@ def main(): before_comp = kc.get_component(cid, realm) if before_comp is None: - before_comp = dict() + before_comp = {} # if user federation exists, get associated mappers if cid is not None: before_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name')) - # build a changeset - changeset = dict() + # Build a proposed changeset from parameters given to this module + changeset = {} for param in comp_params: 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: module.fail_json(msg='Either `name` or `id` has to be specified on each mapper.') if cid is None: - old_mapper = dict() + old_mapper = {} elif change.get('id') is not None: old_mapper = kc.get_component(change['id'], realm) if old_mapper is None: - old_mapper = dict() + old_mapper = {} else: found = kc.get_components(urlencode(dict(parent=cid, name=change['name'])), realm) if len(found) > 1: @@ -863,7 +862,7 @@ def main(): if len(found) == 1: old_mapper = found[0] else: - old_mapper = dict() + old_mapper = {} new_mapper = old_mapper.copy() new_mapper.update(change) if new_mapper != old_mapper: @@ -871,37 +870,37 @@ def main(): changeset['mappers'] = list() changeset['mappers'].append(new_mapper) - # prepare the new representation - updated_comp = before_comp.copy() - updated_comp.update(changeset) + # 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.update(changeset) result['proposed'] = sanitize(changeset) result['existing'] = sanitize(before_comp) - # if before_comp is none, the user federation doesn't exist. - if before_comp == dict(): + # Cater for when it doesn't exist (an empty dict) + if not before_comp: if state == 'absent': - # nothing to do. + # Do nothing and exit if module._diff: result['diff'] = dict(before='', after='') result['changed'] = False - result['end_state'] = dict() + result['end_state'] = {} result['msg'] = 'User federation does not exist; doing nothing.' module.exit_json(**result) - # for 'present', create a new user federation. + # Process a creation result['changed'] = True if module._diff: - result['diff'] = dict(before='', after=sanitize(updated_comp)) + result['diff'] = dict(before='', after=sanitize(desired_comp)) if module.check_mode: module.exit_json(**result) - # do it for real! - updated_comp = updated_comp.copy() - updated_mappers = updated_comp.pop('mappers', []) - after_comp = kc.create_component(updated_comp, realm) + # create it + desired_comp = desired_comp.copy() + updated_mappers = desired_comp.pop('mappers', []) + after_comp = kc.create_component(desired_comp, realm) for mapper in updated_mappers: if mapper.get('id') is not None: @@ -919,26 +918,28 @@ def main(): else: if state == 'present': + # Process an update + # no changes - if updated_comp == before_comp: + if desired_comp == before_comp: 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) module.exit_json(**result) - # update the existing role + # doing an update result['changed'] = True 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: module.exit_json(**result) # do the update - updated_comp = updated_comp.copy() - updated_mappers = updated_comp.pop('mappers', []) - kc.update_component(updated_comp, realm) + desired_comp = desired_comp.copy() + updated_mappers = desired_comp.pop('mappers', []) + kc.update_component(desired_comp, realm) after_comp = kc.get_component(cid, realm) for mapper in updated_mappers: @@ -946,7 +947,7 @@ def main(): kc.update_component(mapper, realm) else: if mapper.get('parentId') is None: - mapper['parentId'] = updated_comp['id'] + mapper['parentId'] = desired_comp['id'] mapper = kc.create_component(mapper, realm) after_comp['mappers'] = updated_mappers @@ -956,6 +957,7 @@ def main(): module.exit_json(**result) elif state == 'absent': + # Process a deletion result['changed'] = True if module._diff: @@ -964,13 +966,12 @@ def main(): if module.check_mode: module.exit_json(**result) - # delete for real + # delete it kc.delete_component(cid, realm) - result['end_state'] = dict() + result['end_state'] = {} result['msg'] = "User federation {id} has been deleted".format(id=cid) - module.exit_json(**result) module.exit_json(**result)