Add keycloak_client module for administration of Keycloak clients (#31716)

Allows administration of Keycloak (http://www.keycloak.org/) clients via the Keycloak REST API
This commit is contained in:
Eike Frost 2017-11-29 22:44:35 +01:00 committed by Adrian Likins
parent 1b31e34d7c
commit 16081d2751
5 changed files with 882 additions and 0 deletions

View file

@ -28,6 +28,7 @@ The following is a list of module_utils files and a general description. The mod
- iosxr.py - Definitions and helper functions for modules that manage Cisco IOS-XR networking devices
- ismount.py - Contains single helper function that fixes os.path.ismount
- junos.py - Definitions and helper functions for modules that manage Junos networking devices
- keycloak.py - Definitions and helper functions for modules working with the Keycloak API
- known_hosts.py - utilities for working with known_hosts file
- manageiq.py - Functions and utilities for modules that work with ManageIQ platform and its resources.
- mlnxos.py - Definitions and helper functions for modules that manage Mellanox MLNX-OS networking devices

View file

@ -0,0 +1,205 @@
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
#
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from ansible.module_utils.urls import open_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.six.moves.urllib.error import HTTPError
URL_TOKEN = "{url}/realms/{realm}/protocol/openid-connect/token"
URL_CLIENT = "{url}/admin/realms/{realm}/clients/{id}"
URL_CLIENTS = "{url}/admin/realms/{realm}/clients"
URL_CLIENT_ROLES = "{url}/admin/realms/{realm}/clients/{id}/roles"
URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles"
def keycloak_argument_spec():
"""
Returns argument_spec of options common to keycloak_*-modules
:return: argument_spec dict
"""
return dict(
auth_keycloak_url=dict(type='str', aliases=['url'], required=True),
auth_client_id=dict(type='str', default='admin-cli'),
auth_realm=dict(type='str', required=True),
auth_client_secret=dict(type='str', default=None),
auth_username=dict(type='str', aliases=['username'], required=True),
auth_password=dict(type='str', aliases=['password'], required=True, no_log=True),
validate_certs=dict(type='bool', default=True)
)
def camel(words):
return words.split('_')[0] + ''.join(x.capitalize() or '_' for x in words.split('_')[1:])
class KeycloakAPI(object):
""" Keycloak API access; Keycloak uses OAuth 2.0 to protect its API, an access token for which
is obtained through OpenID connect
"""
def __init__(self, module):
self.module = module
self.token = None
self._connect()
def _connect(self):
""" Obtains an access_token and saves it for use in API accesses
"""
self.baseurl = self.module.params.get('auth_keycloak_url')
self.validate_certs = self.module.params.get('validate_certs')
auth_url = URL_TOKEN.format(url=self.baseurl, realm=self.module.params.get('auth_realm'))
payload = {'grant_type': 'password',
'client_id': self.module.params.get('auth_client_id'),
'client_secret': self.module.params.get('auth_client_secret'),
'username': self.module.params.get('auth_username'),
'password': self.module.params.get('auth_password')}
# Remove empty items, for instance missing client_secret
payload = dict((k, v) for k, v in payload.items() if v is not None)
try:
r = json.load(open_url(auth_url, method='POST',
validate_certs=self.validate_certs, data=urlencode(payload)))
except Exception as e:
self.module.fail_json(msg='Could not obtain access token from %s: %s'
% (auth_url, str(e)))
if 'access_token' in r:
self.token = r['access_token']
self.restheaders = {'Authorization': 'Bearer ' + self.token,
'Content-Type': 'application/json'}
else:
self.module.fail_json(msg='Could not obtain access token from %s' % auth_url)
def get_clients(self, realm='master', filter=None):
""" Obtains client representations for clients in a realm
:param realm: realm to be queried
:param filter: if defined, only the client with clientId specified in the filter is returned
:return: list of dicts of client representations
"""
clientlist_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
if filter is not None:
clientlist_url += '?clientId=%s' % filter
try:
return json.load(open_url(clientlist_url, method='GET', headers=self.restheaders,
validate_certs=self.validate_certs))
except Exception as e:
self.module.fail_json(msg='Could not obtain list of clients for realm %s: %s'
% (realm, str(e)))
def get_client_by_clientid(self, client_id, realm='master'):
""" Get client representation by clientId
:param client_id: The clientId to be queried
:param realm: realm from which to obtain the client representation
:return: dict with a client representation or None if none matching exist
"""
r = self.get_clients(realm=realm, filter=client_id)
if len(r) > 0:
return r[0]
else:
return None
def get_client_by_id(self, id, realm='master'):
""" Obtain client representatio by id
:param id: id (not clientId) of client to be queried
:param realm: client from this realm
:return: dict of client representation or None if none matching exist
"""
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
try:
return json.load(open_url(client_url, method='GET', headers=self.restheaders,
validate_certs=self.validate_certs))
except HTTPError as e:
if e.code == 404:
return None
else:
self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
% (id, realm, str(e)))
except Exception as e:
self.module.fail_json(msg='Could not obtain client %s for realm %s: %s'
% (id, realm, str(e)))
def update_client(self, id, clientrep, realm="master"):
""" Update an existing client
:param id: id (not clientId) of client to be updated in Keycloak
:param clientrep: corresponding (partial/full) client representation with updates
:param realm: realm the client is in
:return: HTTPResponse object on success
"""
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
try:
return open_url(client_url, method='PUT', headers=self.restheaders,
data=json.dumps(clientrep), validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Could not update client %s in realm %s: %s'
% (id, realm, str(e)))
def create_client(self, clientrep, realm="master"):
""" Create a client in keycloak
:param clientrep: Client representation of client to be created. Must at least contain field clientId
:param realm: realm for client to be created
:return: HTTPResponse object on success
"""
client_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
try:
return open_url(client_url, method='POST', headers=self.restheaders,
data=json.dumps(clientrep), validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Could not create client %s in realm %s: %s'
% (clientrep['clientId'], realm, str(e)))
def delete_client(self, id, realm="master"):
""" Delete a client from Keycloak
:param id: id (not clientId) of client to be deleted
:param realm: realm of client to be deleted
:return: HTTPResponse object on success
"""
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
try:
return open_url(client_url, method='DELETE', headers=self.restheaders,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Could not delete client %s in realm %s: %s'
% (id, realm, str(e)))

View file

@ -0,0 +1,617 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Eike Frost <ei@kefro.st>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: keycloak_client
short_description: Allows administration of Keycloak clients via Keycloak API
version_added: "2.5"
description:
- This module allows the administration of Keycloak clients via the Keycloak REST API. It
requires access to the REST API via OpenID Connect; the user connecting and the client being
used must have the requisite access rights. In a default Keycloak installation, admin-cli
and an admin user would work, as would a separate client definition with the scope tailored
to your needs and a user having the expected roles.
- The names of module options are snake_cased versions of the camelCase ones found in the
Keycloak API and its documentation at U(http://www.keycloak.org/docs-api/3.3/rest-api/)
- The Keycloak API does not always enforce for only sensible settings to be used -- you can set
SAML-specific settings on an OpenID Connect client for instance and vice versa. Be careful.
If you do not specify a setting, usually a sensible default is chosen.
options:
state:
description:
- State of the client
- On C(present), the client will be created (or updated if it exists already).
- On C(absent), the client will be removed if it exists
required: false
choices: ['present', 'absent']
default: 'present'
client_id:
description:
- Client id of client to be worked on. This is usually an alphanumeric name chosen by
you. Either this or I(id) is required. If you specify both, I(id) takes precedence.
This is 'clientId' in the Keycloak REST API.
required: false
id:
description:
- Id of client to be worked on. This is usually an UUID. Either this or I(client_id)
is required. If you specify both, this takes precedence.
required: false
name:
description:
- Name of the client (this is not the same as I(client_id))
required: false
description:
description:
- Description of the client in Keycloak
required: false
root_url:
description:
- Root URL appended to relative URLs for this client
This is 'rootUrl' in the Keycloak REST API.
required: false
admin_url:
description:
- URL to the admin interface of the client
This is 'adminUrl' in the Keycloak REST API.
required: false
base_url:
description:
- Default URL to use when the auth server needs to redirect or link back to the client
This is 'baseUrl' in the Keycloak REST API.
required: false
enabled:
description:
- Is this client enabled or not?
required: false
client_authenticator_type:
description:
- How do clients authenticate with the auth server? Either C(client-secret) or
C(client-jwt) can be chosen. When using C(client-secret), the module parameter
I(secret) can set it, while for C(client-jwt), you can use the keys C(use.jwks.url),
C(jwks.url), and C(jwt.credential.certificate) in the I(attributes) module parameter
to configure its behavior.
This is 'clientAuthenticatorType' in the Keycloak REST API.
required: false
choices: ['client-secret', 'client-jwt']
secret:
description:
- When using I(client_authenticator_type) C(client-secret) (the default), you can
specify a secret here (otherwise one will be generated if it does not exit). If
changing this secret, the module will not register a change currently (but the
changed secret will be saved).
required: false
registration_access_token:
description:
- The registration access token provides access for clients to the client registration
service.
This is 'registrationAccessToken' in the Keycloak REST API.
required: false
default_roles:
description:
- list of default roles for this client. If the client roles referenced do not exist
yet, they will be created.
This is 'defaultRoles' in the Keycloak REST API.
required: false
redirect_uris:
description:
- Acceptable redirect URIs for this client.
This is 'redirectUris' in the Keycloak REST API.
required: false
web_origins:
description:
- List of allowed CORS origins.
This is 'webOrigins' in the Keycloak REST API.
required: false
not_before:
description:
- Revoke any tokens issued before this date for this client (this is a UNIX timestamp).
This is 'notBefore' in the Keycloak REST API.
required: false
bearer_only:
description:
- The access type of this client is bearer-only.
This is 'bearerOnly' in the Keycloak REST API.
required: false
consent_required:
description:
- If enabled, users have to consent to client access.
This is 'consentRequired' in the Keycloak REST API.
required: false
standard_flow_enabled:
description:
- Enable standard flow for this client or not (OpenID connect).
This is 'standardFlowEnabled' in the Keycloak REST API.
required: false
implicit_flow_enabled:
description:
- Enable implicit flow for this client or not (OpenID connect).
This is 'implictFlowEnabled' in the Keycloak REST API.
required: false
direct_access_grants_enabled:
description:
- Are direct access grants enabled for this client or not (OpenID connect).
This is 'directAccessGrantsEnabled' in the Keycloak REST API.
required: false
service_accounts_enabled:
description:
- Are service accounts enabled for this client or not (OpenID connect).
This is 'serviceAccountsEnabled' in the Keycloak REST API.
required: false
authorization_services_enabled:
description:
- Are authorization services enabled for this client or not (OpenID connect).
This is 'authorizationServicesEnabled' in the Keycloak REST API.
required: false
public_client:
description:
- Is the access type for this client public or not.
This is 'publicClient' in the Keycloak REST API.
required: false
frontchannel_logout:
description:
- Is frontchannel logout enabled for this client or not.
This is 'frontchannelLogout' in the Keycloak REST API.
required: false
protocol:
description:
- Type of client (either C(openid-connect) or C(saml).
required: false
choices: ['openid-connect', 'saml']
full_scope_allowed:
description:
- Is the "Full Scope Allowed" feature set for this client or not.
This is 'fullScopeAllowed' in the Keycloak REST API.
required: false
node_re_registration_timeout:
description:
- Cluster node re-registration timeout for this client.
This is 'nodeReRegistrationTimeout' in the Keycloak REST API.
required: false
registered_nodes:
description:
- dict of registered cluster nodes (with C(nodename) as the key and last registration
time as the value).
This is 'registeredNodes' in the Keycloak REST API.
required: false
client_template:
description:
- Client template to use for this client. If it does not exist this field will silently
be dropped.
This is 'clientTemplate' in the Keycloak REST API.
required: false
use_template_config:
description:
- Whether or not to use configuration from the I(client_template).
This is 'useTemplateConfig' in the Keycloak REST API.
required: false
use_template_scope:
description:
- Whether or not to use scope configuration from the I(client_template).
This is 'useTemplateScope' in the Keycloak REST API.
required: false
use_template_mappers:
description:
- Whether or not to use mapper configuration from the I(client_template).
This is 'useTemplateMappers' in the Keycloak REST API.
required: false
surrogate_auth_required:
description:
- Whether or not surrogate auth is required.
This is 'surrogateAuthRequired' in the Keycloak REST API.
required: false
authorization_settings:
description:
- a data structure defining the authorization settings for this client. For reference,
please see the Keycloak API docs at U(http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_resourceserverrepresentation).
This is 'authorizationSettings' in the Keycloak REST API.
required: false
protocol_mappers:
description:
- a list of dicts defining protocol mappers for this client. An example of one is given
in the examples section.
This is 'protocolMappers' in the Keycloak REST API.
required: false
attributes:
description:
- A dict of further attributes for this client. This can contain various configuration
settings; an example is given in the examples section.
required: false
extends_documentation_fragment:
- keycloak
author:
- Eike Frost (@eikef)
'''
EXAMPLES = '''
- name: Create or update Keycloak client (minimal example)
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
client_id: test
state: present
- name: Delete a Keycloak client
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
client_id: test
state: absent
- name: Create or update a Keycloak client (with all the bells and whistles)
local_action:
module: keycloak_client
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
auth_realm: master
auth_username: USERNAME
auth_password: PASSWORD
state: present
realm: master
client_id: test
id: d8b127a3-31f6-44c8-a7e4-4ab9a3e78d95
name: this_is_a_test
description: Description of this wonderful client
root_url: https://www.example.com/
admin_url: https://www.example.com/admin_url
base_url: basepath
enabled: True
client_authenticator_type: client-secret
secret: REALLYWELLKEPTSECRET
redirect_uris:
- https://www.example.com/*
- http://localhost:8888/
web_origins:
- https://www.example.com/*
not_before: 1507825725
bearer_only: False
consent_required: False
standard_flow_enabled: True
implicit_flow_enabled: False
direct_access_grants_enabled: False
service_accounts_enabled: False
authorization_services_enabled: False
public_client: False
frontchannel_logout: False
protocol: openid-connect
full_scope_allowed: false
node_re_registration_timeout: -1
client_template: test
use_template_config: False
use_template_scope: false
use_template_mappers: no
registered_nodes:
node01.example.com: 1507828202
registration_access_token: eyJWT_TOKEN
surrogate_auth_required: false
default_roles:
- test01
- test02
protocol_mappers:
- config:
access.token.claim: True
claim.name: "family_name"
id.token.claim: True
jsonType.label: String
user.attribute: lastName
userinfo.token.claim: True
consentRequired: True
consentText: "${familyName}"
name: family name
protocol: openid-connect
protocolMapper: oidc-usermodel-property-mapper
- config:
attribute.name: Role
attribute.nameformat: Basic
single: false
consentRequired: false
name: role list
protocol: saml
protocolMapper: saml-role-list-mapper
attributes:
saml.authnstatement: True
saml.client.signature: True
saml.force.post.binding: True
saml.server.signature: True
saml.signature.algorithm: RSA_SHA256
saml.signing.certificate: CERTIFICATEHERE
saml.signing.private.key: PRIVATEKEYHERE
saml_force_name_id_format: False
saml_name_id_format: username
saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#"
user.info.response.signature.alg: RS256
request.object.signature.alg: RS256
use.jwks.url: true
jwks.url: JWKS_URL_FOR_CLIENT_AUTH_JWT
jwt.credential.certificate: JWT_CREDENTIAL_CERTIFICATE_FOR_CLIENT_AUTH
'''
RETURN = '''
msg:
description: Message as to what action was taken
returned: always
type: string
sample: "Client testclient has been updated"
proposed:
description: client representation of proposed changes to client
returned: always
type: dict
sample: {
clientId: "test"
}
existing:
description: client representation of existing client (sample is truncated)
returned: always
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
end_state:
description: client representation of client after module execution (sample is truncated)
returned: always
type: dict
sample: {
"adminUrl": "http://www.example.com/admin_url",
"attributes": {
"request.object.signature.alg": "RS256",
}
}
'''
from ansible.module_utils.keycloak import KeycloakAPI, camel, keycloak_argument_spec
from ansible.module_utils.basic import AnsibleModule
def sanitize_cr(clientrep):
""" Removes probably sensitive details from a client representation
:param clientrep: the clientrep dict to be sanitized
:return: sanitized clientrep dict
"""
result = clientrep.copy()
if 'secret' in result:
result['secret'] = 'no_log'
if 'attributes' in result:
if 'saml.signing.private.key' in result['attributes']:
result['attributes']['saml.signing.private.key'] = 'no_log'
return result
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
meta_args = dict(
state=dict(default='present', choices=['present', 'absent']),
realm=dict(type='str', default='master'),
id=dict(type='str'),
client_id=dict(type='str'),
name=dict(type='str'),
description=dict(type='str'),
root_url=dict(type='str'),
admin_url=dict(type='str'),
base_url=dict(type='str'),
surrogate_auth_required=dict(type='bool'),
enabled=dict(type='bool'),
client_authenticator_type=dict(type='str', choices=['client-secret', 'client-jwt']),
secret=dict(type='str', no_log=True),
registration_access_token=dict(type='str'),
default_roles=dict(type='list'),
redirect_uris=dict(type='list'),
web_origins=dict(type='list'),
not_before=dict(type='int'),
bearer_only=dict(type='bool'),
consent_required=dict(type='bool'),
standard_flow_enabled=dict(type='bool'),
implicit_flow_enabled=dict(type='bool'),
direct_access_grants_enabled=dict(type='bool'),
service_accounts_enabled=dict(type='bool'),
authorization_services_enabled=dict(type='bool'),
public_client=dict(type='bool'),
frontchannel_logout=dict(type='bool'),
protocol=dict(type='str', choices=['openid-connect', 'saml']),
attributes=dict(type='dict'),
full_scope_allowed=dict(type='bool'),
node_re_registration_timeout=dict(type='int'),
registered_nodes=dict(type='dict'),
client_template=dict(type='str'),
use_template_config=dict(type='bool'),
use_template_scope=dict(type='bool'),
use_template_mappers=dict(type='bool'),
protocol_mappers=dict(type='list'),
authorization_settings=dict(type='dict'),
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['client_id', 'id']]))
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API
kc = KeycloakAPI(module)
realm = module.params.get('realm')
cid = module.params.get('id')
state = module.params.get('state')
# convert module parameters to client representation parameters (if they belong in there)
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
if cid is None:
before_client = kc.get_client_by_clientid(module.params.get('client_id'), realm=realm)
if before_client is not None:
cid = before_client['id']
else:
before_client = kc.get_client_by_id(cid, realm=realm)
if before_client is None:
before_client = dict()
# Build a proposed changeset from parameters given to this module
changeset = dict()
for client_param in client_params:
# lists in the Keycloak API are sorted
new_param_value = module.params.get(client_param)
if isinstance(new_param_value, list):
try:
new_param_value = sorted(new_param_value)
except TypeError:
pass
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)
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():
if state == 'absent':
# do nothing and exit
if module._diff:
result['diff'] = dict(before='', after='')
result['msg'] = 'Client does not exist, doing nothing.'
module.exit_json(**result)
# create new client
result['changed'] = True
if 'clientId' not in updated_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))
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)
result['end_state'] = sanitize_cr(after_client)
result['msg'] = 'Client %s has been created.' % updated_client['clientId']
module.exit_json(**result)
else:
if state == 'present':
# update existing client
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))
module.exit_json(**result)
kc.update_client(cid, updated_client, realm=realm)
after_client = kc.get_client_by_id(cid, realm=realm)
if before_client == after_client:
result['changed'] = False
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']
module.exit_json(**result)
else:
# Delete existing client
result['changed'] = True
if module._diff:
result['diff']['before'] = sanitize_cr(before_client)
result['diff']['after'] = ''
if module.check_mode:
module.exit_json(**result)
kc.delete_client(cid, realm=realm)
result['proposed'] = dict()
result['end_state'] = dict()
result['msg'] = 'Client %s has been deleted.' % before_client['clientId']
module.exit_json(**result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,59 @@
# Copyright (c) 2017 Eike Frost <ei@kefro.st>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
class ModuleDocFragment(object):
# Standard documentation fragment
DOCUMENTATION = '''
options:
auth_keycloak_url:
description:
- URL to the Keycloak instance.
required: true
auth_client_id:
description:
- OpenID Connect I(client_id) to authenticate to the API with.
required: true
auth_realm:
description:
- Keycloak realm name to authenticate to for API access.
required: true
auth_client_secret:
description:
- Client Secret to use in conjunction with I(auth_client_id) (if required).
required: false
auth_username:
description:
- Username to authenticate for API access with.
required: true
auth_password:
description:
- Password to authenticate for API access with.
required: true
validate_certs:
description:
- Verify TLS certificates (do not disable this in production).
required: false
default: True
'''