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 = '''
---
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"])

View file

@ -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)

View file

@ -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",

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)