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

Add keycloak_user_federation module (#3340)

* new module

* fix unit tests

* fix documentation

* more fixes

* fix linefeeds

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* use true/false instead of True/False

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* fix result content + rename variable

* urlencode parameters

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Laurent Paumier 2021-09-20 19:19:42 +02:00 committed by GitHub
parent 02d0e3d286
commit 2589e9a030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1889 additions and 0 deletions

2
.github/BOTMETA.yml vendored
View file

@ -521,6 +521,8 @@ files:
maintainers: kris2kris maintainers: kris2kris
$modules/identity/keycloak/keycloak_role.py: $modules/identity/keycloak/keycloak_role.py:
maintainers: laurpaum maintainers: laurpaum
$modules/identity/keycloak/keycloak_user_federation.py:
maintainers: laurpaum
$modules/identity/onepassword_info.py: $modules/identity/onepassword_info.py:
maintainers: Rylon maintainers: Rylon
$modules/identity/opendj/opendj_backendprop.py: $modules/identity/opendj/opendj_backendprop.py:

View file

@ -83,6 +83,9 @@ URL_IDENTITY_PROVIDER = "{url}/admin/realms/{realm}/identity-provider/instances/
URL_IDENTITY_PROVIDER_MAPPERS = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers" URL_IDENTITY_PROVIDER_MAPPERS = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers"
URL_IDENTITY_PROVIDER_MAPPER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}" URL_IDENTITY_PROVIDER_MAPPER = "{url}/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}"
URL_COMPONENTS = "{url}/admin/realms/{realm}/components"
URL_COMPONENT = "{url}/admin/realms/{realm}/components/{id}"
def keycloak_argument_spec(): def keycloak_argument_spec():
""" """
@ -1601,3 +1604,93 @@ class KeycloakAPI(object):
except Exception as e: except Exception as e:
self.module.fail_json(msg='Unable to delete mapper %s for identity provider %s in realm %s: %s' self.module.fail_json(msg='Unable to delete mapper %s for identity provider %s in realm %s: %s'
% (mid, alias, realm, str(e))) % (mid, alias, realm, str(e)))
def get_components(self, filter=None, realm='master'):
""" Fetch representations for components in a realm
:param realm: realm to be queried
:param filter: search filter
:return: list of representations for components
"""
comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm)
if filter is not None:
comps_url += '?%s' % filter
try:
return json.loads(to_native(open_url(comps_url, method='GET', headers=self.restheaders,
validate_certs=self.validate_certs).read()))
except ValueError as e:
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of components for realm %s: %s'
% (realm, str(e)))
except Exception as e:
self.module.fail_json(msg='Could not obtain list of components for realm %s: %s'
% (realm, str(e)))
def get_component(self, cid, realm='master'):
""" Fetch component representation from a realm using its cid.
If the component does not exist, None is returned.
:param cid: Unique ID of the component to fetch.
:param realm: Realm in which the component resides; default 'master'.
"""
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
try:
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
validate_certs=self.validate_certs).read()))
except HTTPError as e:
if e.code == 404:
return None
else:
self.module.fail_json(msg='Could not fetch component %s in realm %s: %s'
% (cid, realm, str(e)))
except Exception as e:
self.module.fail_json(msg='Could not fetch component %s in realm %s: %s'
% (cid, realm, str(e)))
def create_component(self, comprep, realm='master'):
""" Create an component.
:param comprep: Component representation of the component to be created.
:param realm: Realm in which this component resides, default "master".
:return: Component representation of the created component
"""
comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm)
try:
resp = open_url(comps_url, method='POST', headers=self.restheaders,
data=json.dumps(comprep), validate_certs=self.validate_certs)
comp_url = resp.getheader('Location')
if comp_url is None:
self.module.fail_json(msg='Could not create component in realm %s: %s'
% (realm, 'unexpected response'))
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
validate_certs=self.validate_certs).read()))
except Exception as e:
self.module.fail_json(msg='Could not create component in realm %s: %s'
% (realm, str(e)))
def update_component(self, comprep, realm='master'):
""" Update an existing component.
:param comprep: Component representation of the component to be updated.
:param realm: Realm in which this component resides, default "master".
:return HTTPResponse object on success
"""
cid = comprep.get('id')
if cid is None:
self.module.fail_json(msg='Cannot update component without id')
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
try:
return open_url(comp_url, method='PUT', headers=self.restheaders,
data=json.dumps(comprep), validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Could not update component %s in realm %s: %s'
% (cid, realm, str(e)))
def delete_component(self, cid, realm='master'):
""" Delete an component.
:param cid: Unique ID of the component.
:param realm: Realm in which this component resides, default "master".
"""
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
try:
return open_url(comp_url, method='DELETE', headers=self.restheaders,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Unable to delete component %s in realm %s: %s'
% (cid, realm, str(e)))

View file

@ -0,0 +1,979 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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
DOCUMENTATION = '''
---
module: keycloak_user_federation
short_description: Allows administration of Keycloak user federations via Keycloak API
version_added: 3.7.0
description:
- This module allows you to add, remove or modify Keycloak user federations 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(https://www.keycloak.org/docs-api/15.0/rest-api/index.html).
options:
state:
description:
- State of the user federation.
- On C(present), the user federation will be created if it does not yet exist, or updated with
the parameters you provide.
- On C(absent), the user federation will be removed if it exists.
default: 'present'
type: str
choices:
- present
- absent
realm:
description:
- The Keycloak realm under which this user federation resides.
default: 'master'
type: str
id:
description:
- The unique ID for this user federation. If left empty, the user federation will be searched
by its I(name).
type: str
name:
description:
- Display name of provider when linked in admin console.
type: str
provider_id:
description:
- Provider for this user federation.
aliases:
- providerId
type: str
choices:
- ldap
- kerberos
provider_type:
description:
- Component type for user federation (only supported value is C(org.keycloak.storage.UserStorageProvider)).
aliases:
- providerType
default: org.keycloak.storage.UserStorageProvider
type: str
parent_id:
description:
- Unique ID for the parent of this user federation. Realm ID will be automatically used if left blank.
aliases:
- parentId
type: str
config:
description:
- Dict specifying the configuration options for the provider; the contents differ depending on
the value of I(provider_id). Examples are given below for C(ldap) and C(kerberos). It is easiest
to obtain valid config values by dumping an already-existing user federation configuration
through check-mode in the I(existing) field.
type: dict
suboptions:
enabled:
description:
- Enable/disable this user federation.
default: true
type: bool
priority:
description:
- Priority of provider when doing a user lookup. Lowest first.
default: 0
type: int
importEnabled:
description:
- If C(true), LDAP users will be imported into Keycloak DB and synced by the configured
sync policies.
default: true
type: bool
editMode:
description:
- C(READ_ONLY) is a read-only LDAP store. C(WRITABLE) means data will be synced back to LDAP
on demand. C(UNSYNCED) means user data will be imported, but not synced back to LDAP.
type: str
choices:
- READ_ONLY
- WRITABLE
- UNSYNCED
syncRegistrations:
description:
- Should newly created users be created within LDAP store? Priority effects which
provider is chosen to sync the new user.
default: false
type: bool
vendor:
description:
- LDAP vendor (provider).
type: str
usernameLDAPAttribute:
description:
- Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server
vendors it can be C(uid). For Active directory it can be C(sAMAccountName) or C(cn).
The attribute should be filled for all LDAP user records you want to import from
LDAP to Keycloak.
type: str
rdnLDAPAttribute:
description:
- Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN.
Usually it's the same as Username LDAP attribute, however it is not required. For
example for Active directory, it is common to use C(cn) as RDN attribute when
username attribute might be C(sAMAccountName).
type: str
uuidLDAPAttribute:
description:
- Name of LDAP attribute, which is used as unique object identifier (UUID) for objects
in LDAP. For many LDAP server vendors, it is C(entryUUID); however some are different.
For example for Active directory it should be C(objectGUID). If your LDAP server does
not support the notion of UUID, you can use any other attribute that is supposed to
be unique among LDAP users in tree.
type: str
userObjectClasses:
description:
- All values of LDAP objectClass attribute for users in LDAP divided by comma.
For example C(inetOrgPerson, organizationalPerson). Newly created Keycloak users
will be written to LDAP with all those object classes and existing LDAP user records
are found just if they contain all those object classes.
type: str
connectionUrl:
description:
- Connection URL to your LDAP server.
type: str
usersDn:
description:
- Full DN of LDAP tree where your users are. This DN is the parent of LDAP users.
type: str
customUserSearchFilter:
description:
- Additional LDAP Filter for filtering searched users. Leave this empty if you don't
need additional filter.
type: str
searchScope:
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
default: '1'
type: str
choices:
- '1'
- '2'
authType:
description:
- Type of the Authentication method used during LDAP Bind operation. It is used in
most of the requests sent to the LDAP server.
default: 'none'
type: str
choices:
- none
- simple
bindDn:
description:
- DN of LDAP user which will be used by Keycloak to access LDAP server.
type: str
bindCredential:
description:
- Password of LDAP admin.
type: str
startTls:
description:
- Encrypts the connection to LDAP using STARTTLS, which will disable connection pooling.
default: false
type: bool
usePasswordModifyExtendedOp:
description:
- Use the LDAPv3 Password Modify Extended Operation (RFC-3062). The password modify
extended operation usually requires that LDAP user already has password in the LDAP
server. So when this is used with 'Sync Registrations', it can be good to add also
'Hardcoded LDAP attribute mapper' with randomly generated initial password.
default: false
type: bool
validatePasswordPolicy:
description:
- Determines if Keycloak should validate the password with the realm password policy
before updating it.
default: false
type: bool
trustEmail:
description:
- If enabled, email provided by this provider is not verified even if verification is
enabled for the realm.
default: false
type: bool
useTruststoreSpi:
description:
- Specifies whether LDAP connection will use the truststore SPI with the truststore
configured in standalone.xml/domain.xml. C(Always) means that it will always use it.
C(Never) means that it will not use it. C(Only for ldaps) means that it will use if
your connection URL use ldaps. Note even if standalone.xml/domain.xml is not
configured, the default Java cacerts or certificate specified by
C(javax.net.ssl.trustStore) property will be used.
default: ldapsOnly
type: str
choices:
- always
- ldapsOnly
- never
connectionTimeout:
description:
- LDAP Connection Timeout in milliseconds.
type: int
readTimeout:
description:
- LDAP Read Timeout in milliseconds. This timeout applies for LDAP read operations.
type: int
pagination:
description:
- Does the LDAP server support pagination.
default: true
type: bool
connectionPooling:
description:
- Determines if Keycloak should use connection pooling for accessing LDAP server.
default: true
type: bool
connectionPoolingAuthentication:
description:
- A list of space-separated authentication types of connections that may be pooled.
type: str
choices:
- none
- simple
- DIGEST-MD5
connectionPoolingDebug:
description:
- A string that indicates the level of debug output to produce. Example valid values are
C(fine) (trace connection creation and removal) and C(all) (all debugging information).
type: str
connectionPoolingInitSize:
description:
- The number of connections per connection identity to create when initially creating a
connection for the identity.
type: int
connectionPoolingMaxSize:
description:
- The maximum number of connections per connection identity that can be maintained
concurrently.
type: int
connectionPoolingPrefSize:
description:
- The preferred number of connections per connection identity that should be maintained
concurrently.
type: int
connectionPoolingProtocol:
description:
- A list of space-separated protocol types of connections that may be pooled.
Valid types are C(plain) and C(ssl).
type: str
connectionPoolingTimeout:
description:
- The number of milliseconds that an idle connection may remain in the pool without
being closed and removed from the pool.
type: int
allowKerberosAuthentication:
description:
- Enable/disable HTTP authentication of users with SPNEGO/Kerberos tokens. The data
about authenticated users will be provisioned from this LDAP server.
default: false
type: bool
kerberosRealm:
description:
- Name of kerberos realm.
type: str
serverPrincipal:
description:
- Full name of server principal for HTTP service including server and domain name. For
example C(HTTP/host.foo.org@FOO.ORG). Use C(*) to accept any service principal in the
KeyTab file.
type: str
keyTab:
description:
- Location of Kerberos KeyTab file containing the credentials of server principal. For
example C(/etc/krb5.keytab).
type: str
debug:
description:
- Enable/disable debug logging to standard output for Krb5LoginModule.
type: bool
useKerberosForPasswordAuthentication:
description:
- Use Kerberos login module for authenticate username/password against Kerberos server
instead of authenticating against LDAP server with Directory Service API.
default: false
type: bool
allowPasswordAuthentication:
description:
- Enable/disable possibility of username/password authentication against Kerberos database.
type: bool
batchSizeForSync:
description:
- Count of LDAP users to be imported from LDAP to Keycloak within a single transaction.
default: 1000
type: int
fullSyncPeriod:
description:
- Period for full synchronization in seconds.
default: -1
type: int
changedSyncPeriod:
description:
- Period for synchronization of changed or newly created LDAP users in seconds.
default: -1
type: int
updateProfileFirstLogin:
description:
- Update profile on first login.
type: bool
cachePolicy:
description:
- Cache Policy for this storage provider.
type: str
default: 'DEFAULT'
choices:
- DEFAULT
- EVICT_DAILY
- EVICT_WEEKLY
- MAX_LIFESPAN
- NO_CACHE
evictionDay:
description:
- Day of the week the entry will become invalid on.
type: str
evictionHour:
description:
- Hour of day the entry will become invalid on.
type: str
evictionMinute:
description:
- Minute of day the entry will become invalid on.
type: str
maxLifespan:
description:
- Max lifespan of cache entry in milliseconds.
type: int
mappers:
description:
- A list of dicts defining mappers associated with this Identity Provider.
type: list
elements: dict
suboptions:
id:
description:
- Unique ID of this mapper.
type: str
name:
description:
- Name of the mapper. If no ID is given, the mapper will be searched by name.
type: str
parentId:
description:
- Unique ID for the parent of this mapper. ID of the user federation will automatically
be used if left blank.
type: str
providerId:
description:
- The mapper type for this mapper (for instance C(user-attribute-ldap-mapper)).
type: str
providerType:
description:
- Component type for this mapper (only supported value is C(org.keycloak.storage.ldap.mappers.LDAPStorageMapper)).
type: str
config:
description:
- Dict specifying the configuration options for the mapper; the contents differ
depending on the value of I(identityProviderMapper).
type: dict
extends_documentation_fragment:
- community.general.keycloak
author:
- Laurent Paumier (@laurpaum)
'''
EXAMPLES = '''
- name: Create LDAP user federation
community.general.keycloak_user_federation:
auth_keycloak_url: https://keycloak.example.com/auth
auth_realm: master
auth_username: admin
auth_password: password
realm: my-realm
name: my-ldap
state: present
provider_id: ldap
provider_type: org.keycloak.storage.UserStorageProvider
config:
priority: 0
enabled: true
cachePolicy: DEFAULT
batchSizeForSync: 1000
editMode: READ_ONLY
importEnabled: true
syncRegistrations: false
vendor: other
usernameLDAPAttribute: uid
rdnLDAPAttribute: uid
uuidLDAPAttribute: entryUUID
userObjectClasses: inetOrgPerson, organizationalPerson
connectionUrl: ldaps://ldap.example.com:636
usersDn: ou=Users,dc=example,dc=com
authType: simple
bindDn: cn=directory reader
bindCredential: password
searchScope: 1
validatePasswordPolicy: false
trustEmail: false
useTruststoreSpi: ldapsOnly
connectionPooling: true
pagination: true
allowKerberosAuthentication: false
debug: false
useKerberosForPasswordAuthentication: false
mappers:
- name: "full name"
providerId: "full-name-ldap-mapper"
providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
config:
ldap.full.name.attribute: cn
read.only: true
write.only: false
- name: Create Kerberos user federation
community.general.keycloak_user_federation:
auth_keycloak_url: https://keycloak.example.com/auth
auth_realm: master
auth_username: admin
auth_password: password
realm: my-realm
name: my-kerberos
state: present
provider_id: kerberos
provider_type: org.keycloak.storage.UserStorageProvider
config:
priority: 0
enabled: true
cachePolicy: DEFAULT
kerberosRealm: EXAMPLE.COM
serverPrincipal: HTTP/host.example.com@EXAMPLE.COM
keyTab: keytab
allowPasswordAuthentication: false
updateProfileFirstLogin: false
- name: Delete user federation
community.general.keycloak_user_federation:
auth_keycloak_url: https://keycloak.example.com/auth
auth_realm: master
auth_username: admin
auth_password: password
realm: my-realm
name: my-federation
state: absent
'''
RETURN = '''
msg:
description: Message as to what action was taken.
returned: always
type: str
sample: "No changes required to user federation 164bb483-c613-482e-80fe-7f1431308799."
proposed:
description: Representation of proposed changes to user federation.
returned: always
type: dict
sample: {
"config": {
"allowKerberosAuthentication": "false",
"authType": "simple",
"batchSizeForSync": "1000",
"bindCredential": "**********",
"bindDn": "cn=directory reader",
"cachePolicy": "DEFAULT",
"connectionPooling": "true",
"connectionUrl": "ldaps://ldap.example.com:636",
"debug": "false",
"editMode": "READ_ONLY",
"enabled": "true",
"importEnabled": "true",
"pagination": "true",
"priority": "0",
"rdnLDAPAttribute": "uid",
"searchScope": "1",
"syncRegistrations": "false",
"trustEmail": "false",
"useKerberosForPasswordAuthentication": "false",
"useTruststoreSpi": "ldapsOnly",
"userObjectClasses": "inetOrgPerson, organizationalPerson",
"usernameLDAPAttribute": "uid",
"usersDn": "ou=Users,dc=example,dc=com",
"uuidLDAPAttribute": "entryUUID",
"validatePasswordPolicy": "false",
"vendor": "other"
},
"name": "ldap",
"providerId": "ldap",
"providerType": "org.keycloak.storage.UserStorageProvider"
}
existing:
description: Representation of existing user federation.
returned: always
type: dict
sample: {
"config": {
"allowKerberosAuthentication": "false",
"authType": "simple",
"batchSizeForSync": "1000",
"bindCredential": "**********",
"bindDn": "cn=directory reader",
"cachePolicy": "DEFAULT",
"changedSyncPeriod": "-1",
"connectionPooling": "true",
"connectionUrl": "ldaps://ldap.example.com:636",
"debug": "false",
"editMode": "READ_ONLY",
"enabled": "true",
"fullSyncPeriod": "-1",
"importEnabled": "true",
"pagination": "true",
"priority": "0",
"rdnLDAPAttribute": "uid",
"searchScope": "1",
"syncRegistrations": "false",
"trustEmail": "false",
"useKerberosForPasswordAuthentication": "false",
"useTruststoreSpi": "ldapsOnly",
"userObjectClasses": "inetOrgPerson, organizationalPerson",
"usernameLDAPAttribute": "uid",
"usersDn": "ou=Users,dc=example,dc=com",
"uuidLDAPAttribute": "entryUUID",
"validatePasswordPolicy": "false",
"vendor": "other"
},
"id": "01122837-9047-4ae4-8ca0-6e2e891a765f",
"mappers": [
{
"config": {
"always.read.value.from.ldap": "false",
"is.mandatory.in.ldap": "false",
"ldap.attribute": "mail",
"read.only": "true",
"user.model.attribute": "email"
},
"id": "17d60ce2-2d44-4c2c-8b1f-1fba601b9a9f",
"name": "email",
"parentId": "01122837-9047-4ae4-8ca0-6e2e891a765f",
"providerId": "user-attribute-ldap-mapper",
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
}
],
"name": "myfed",
"parentId": "myrealm",
"providerId": "ldap",
"providerType": "org.keycloak.storage.UserStorageProvider"
}
end_state:
description: Representation of user federation after module execution.
returned: always
type: dict
sample: {
"config": {
"allowPasswordAuthentication": "false",
"cachePolicy": "DEFAULT",
"enabled": "true",
"kerberosRealm": "EXAMPLE.COM",
"keyTab": "/etc/krb5.keytab",
"priority": "0",
"serverPrincipal": "HTTP/host.example.com@EXAMPLE.COM",
"updateProfileFirstLogin": "false"
},
"id": "cf52ae4f-4471-4435-a0cf-bb620cadc122",
"mappers": [],
"name": "kerberos",
"parentId": "myrealm",
"providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider"
}
'''
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
keycloak_argument_spec, get_token, KeycloakError
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import urlencode
from copy import deepcopy
def sanitize(comp):
compcopy = deepcopy(comp)
if 'config' in compcopy:
compcopy['config'] = dict((k, v[0]) for k, v in compcopy['config'].items())
if 'bindCredential' in compcopy['config']:
compcopy['config']['bindCredential'] = '**********'
if 'mappers' in compcopy:
for mapper in compcopy['mappers']:
if 'config' in mapper:
mapper['config'] = dict((k, v[0]) for k, v in mapper['config'].items())
return compcopy
def main():
"""
Module execution
:return:
"""
argument_spec = keycloak_argument_spec()
config_spec = dict(
allowKerberosAuthentication=dict(type='bool', default=False),
allowPasswordAuthentication=dict(type='bool'),
authType=dict(type='str', choices=['none', 'simple'], default='none'),
batchSizeForSync=dict(type='int', default=1000),
bindCredential=dict(type='str', no_log=True),
bindDn=dict(type='str'),
cachePolicy=dict(type='str', choices=['DEFAULT', 'EVICT_DAILY', 'EVICT_WEEKLY', 'MAX_LIFESPAN', 'NO_CACHE'], default='DEFAULT'),
changedSyncPeriod=dict(type='int', default=-1),
connectionPooling=dict(type='bool', default=True),
connectionPoolingAuthentication=dict(type='str', choices=['none', 'simple', 'DIGEST-MD5']),
connectionPoolingDebug=dict(type='str'),
connectionPoolingInitSize=dict(type='int'),
connectionPoolingMaxSize=dict(type='int'),
connectionPoolingPrefSize=dict(type='int'),
connectionPoolingProtocol=dict(type='str'),
connectionPoolingTimeout=dict(type='int'),
connectionTimeout=dict(type='int'),
connectionUrl=dict(type='str'),
customUserSearchFilter=dict(type='str'),
debug=dict(type='bool'),
editMode=dict(type='str', choices=['READ_ONLY', 'WRITABLE', 'UNSYNCED']),
enabled=dict(type='bool', default=True),
evictionDay=dict(type='str'),
evictionHour=dict(type='str'),
evictionMinute=dict(type='str'),
fullSyncPeriod=dict(type='int', default=-1),
importEnabled=dict(type='bool', default=True),
kerberosRealm=dict(type='str'),
keyTab=dict(type='str', no_log=False),
maxLifespan=dict(type='int'),
pagination=dict(type='bool', default=True),
priority=dict(type='int', default=0),
rdnLDAPAttribute=dict(type='str'),
readTimeout=dict(type='int'),
searchScope=dict(type='str', choices=['1', '2'], default='1'),
serverPrincipal=dict(type='str'),
startTls=dict(type='bool', default=False),
syncRegistrations=dict(type='bool', default=False),
trustEmail=dict(type='bool', default=False),
updateProfileFirstLogin=dict(type='bool'),
useKerberosForPasswordAuthentication=dict(type='bool', default=False),
usePasswordModifyExtendedOp=dict(type='bool', default=False, no_log=False),
useTruststoreSpi=dict(type='str', choices=['always', 'ldapsOnly', 'never'], default='ldapsOnly'),
userObjectClasses=dict(type='str'),
usernameLDAPAttribute=dict(type='str'),
usersDn=dict(type='str'),
uuidLDAPAttribute=dict(type='str'),
validatePasswordPolicy=dict(type='bool', default=False),
vendor=dict(type='str'),
)
mapper_spec = dict(
id=dict(type='str'),
name=dict(type='str'),
parentId=dict(type='str'),
providerId=dict(type='str'),
providerType=dict(type='str'),
config=dict(type='dict'),
)
meta_args = dict(
config=dict(type='dict', options=config_spec),
state=dict(type='str', default='present', choices=['present', 'absent']),
realm=dict(type='str', default='master'),
id=dict(type='str'),
name=dict(type='str'),
provider_id=dict(type='str', aliases=['providerId'], choices=['ldap', 'kerberos']),
provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'),
parent_id=dict(type='str', aliases=['parentId']),
mappers=dict(type='list', elements='dict', options=mapper_spec),
)
argument_spec.update(meta_args)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
required_one_of=([['id', 'name'],
['token', 'auth_realm', 'auth_username', 'auth_password']]),
required_together=([['auth_realm', 'auth_username', 'auth_password']]))
result = dict(changed=False, msg='', diff={}, proposed={}, existing={}, end_state={})
# Obtain access token, initialize API
try:
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')
config = module.params.get('config')
mappers = module.params.get('mappers')
cid = module.params.get('id')
name = module.params.get('name')
# Keycloak API expects config parameters to be arrays containing a single string element
if config is not None:
module.params['config'] = dict((k, [str(v).lower() if not isinstance(v, str) else v])
for k, v in config.items() if config[k] is not None)
if mappers is not None:
for mapper in mappers:
if mapper.get('config') is not None:
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)
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?
if cid is None:
found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', parent=realm, name=name)), realm)
if len(found) > 1:
module.fail_json(msg='No ID given and found multiple user federations with name `{name}`. Cannot continue.'.format(name=name))
before_comp = next(iter(found), None)
if before_comp is not None:
cid = before_comp['id']
else:
before_comp = kc.get_component(cid, realm)
if before_comp is None:
before_comp = dict()
# 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()
for param in comp_params:
new_param_value = module.params.get(param)
old_value = before_comp[camel(param)] if camel(param) in before_comp else None
if param == 'mappers':
new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value]
if new_param_value != old_value:
changeset[camel(param)] = new_param_value
# special handling of mappers list to allow change detection
if module.params.get('mappers') is not None:
if module.params['provider_id'] == 'kerberos':
module.fail_json(msg='Cannot configure mappers for Kerberos federations.')
for change in module.params['mappers']:
change = dict((k, v) for k, v in change.items() if change[k] is not None)
if change.get('id') is None and change.get('name') is None:
module.fail_json(msg='Either `name` or `id` has to be specified on each mapper.')
if cid is None:
old_mapper = dict()
elif change.get('id') is not None:
old_mapper = kc.get_component(change['id'], realm)
if old_mapper is None:
old_mapper = dict()
else:
found = kc.get_components(urlencode(dict(parent=cid, name=change['name'])), realm)
if len(found) > 1:
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=change['name']))
if len(found) == 1:
old_mapper = found[0]
else:
old_mapper = dict()
new_mapper = old_mapper.copy()
new_mapper.update(change)
if new_mapper != old_mapper:
if changeset.get('mappers') is None:
changeset['mappers'] = list()
changeset['mappers'].append(new_mapper)
# prepare the new representation
updated_comp = before_comp.copy()
updated_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():
if state == 'absent':
# nothing to do.
if module._diff:
result['diff'] = dict(before='', after='')
result['changed'] = False
result['end_state'] = dict()
result['msg'] = 'User federation does not exist; doing nothing.'
module.exit_json(**result)
# for 'present', create a new user federation.
result['changed'] = True
if module._diff:
result['diff'] = dict(before='', after=sanitize(updated_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)
for mapper in updated_mappers:
if mapper.get('id') is not None:
kc.update_component(mapper, realm)
else:
if mapper.get('parentId') is None:
mapper['parentId'] = after_comp['id']
mapper = kc.create_component(mapper, realm)
after_comp['mappers'] = updated_mappers
result['end_state'] = sanitize(after_comp)
result['msg'] = "User federation {id} has been created".format(id=after_comp['id'])
module.exit_json(**result)
else:
if state == 'present':
# no changes
if updated_comp == before_comp:
result['changed'] = False
result['end_state'] = sanitize(updated_comp)
result['msg'] = "No changes required to user federation {id}.".format(id=cid)
module.exit_json(**result)
# update the existing role
result['changed'] = True
if module._diff:
result['diff'] = dict(before=sanitize(before_comp), after=sanitize(updated_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)
after_comp = kc.get_component(cid, realm)
for mapper in updated_mappers:
if mapper.get('id') is not None:
kc.update_component(mapper, realm)
else:
if mapper.get('parentId') is None:
mapper['parentId'] = updated_comp['id']
mapper = kc.create_component(mapper, realm)
after_comp['mappers'] = updated_mappers
result['end_state'] = sanitize(after_comp)
result['msg'] = "User federation {id} has been updated".format(id=cid)
module.exit_json(**result)
elif state == 'absent':
result['changed'] = True
if module._diff:
result['diff'] = dict(before=sanitize(before_comp), after='')
if module.check_mode:
module.exit_json(**result)
# delete for real
kc.delete_component(cid, realm)
result['end_state'] = dict()
result['msg'] = "User federation {id} has been deleted".format(id=cid)
module.exit_json(**result)
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
identity/keycloak/keycloak_user_federation.py

View file

@ -0,0 +1 @@
unsupported

View file

@ -0,0 +1,225 @@
---
- name: Create realm
community.general.keycloak_realm:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
id: "{{ realm }}"
realm: "{{ realm }}"
state: present
- name: Create new user federation
community.general.keycloak_user_federation:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ federation }}"
state: present
provider_id: ldap
provider_type: org.keycloak.storage.UserStorageProvider
config:
enabled: true
priority: 0
fullSyncPeriod: -1
changedSyncPeriod: -1
cachePolicy: DEFAULT
batchSizeForSync: 1000
editMode: READ_ONLY
importEnabled: true
syncRegistrations: false
vendor: other
usernameLDAPAttribute: uid
rdnLDAPAttribute: uid
uuidLDAPAttribute: entryUUID
userObjectClasses: "inetOrgPerson, organizationalPerson"
connectionUrl: "ldaps://ldap.example.com:636"
usersDn: "ou=Users,dc=example,dc=com"
authType: simple
bindDn: cn=directory reader
bindCredential: secret
searchScope: 1
validatePasswordPolicy: false
trustEmail: false
useTruststoreSpi: "ldapsOnly"
connectionPooling: true
pagination: true
allowKerberosAuthentication: false
useKerberosForPasswordAuthentication: false
debug: false
register: result
- name: Debug
debug:
var: result
- name: Assert user federation created
assert:
that:
- result is changed
- result.existing == {}
- result.end_state.name == "{{ federation }}"
- name: Update existing user federation (no change)
community.general.keycloak_user_federation:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ federation }}"
state: present
provider_id: ldap
provider_type: org.keycloak.storage.UserStorageProvider
config:
enabled: true
priority: 0
fullSyncPeriod: -1
changedSyncPeriod: -1
cachePolicy: DEFAULT
batchSizeForSync: 1000
editMode: READ_ONLY
importEnabled: true
syncRegistrations: false
vendor: other
usernameLDAPAttribute: uid
rdnLDAPAttribute: uid
uuidLDAPAttribute: entryUUID
userObjectClasses: "inetOrgPerson, organizationalPerson"
connectionUrl: "ldaps://ldap.example.com:636"
usersDn: "ou=Users,dc=example,dc=com"
authType: simple
bindDn: cn=directory reader
bindCredential: "**********"
searchScope: 1
validatePasswordPolicy: false
trustEmail: false
useTruststoreSpi: "ldapsOnly"
connectionPooling: true
pagination: true
allowKerberosAuthentication: false
useKerberosForPasswordAuthentication: false
debug: false
register: result
- name: Debug
debug:
var: result
- name: Assert user federation unchanged
assert:
that:
- result is not changed
- result.existing != {}
- result.existing.name == "{{ federation }}"
- result.end_state != {}
- result.end_state.name == "{{ federation }}"
- name: Update existing user federation (with change)
community.general.keycloak_user_federation:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ federation }}"
state: present
provider_id: ldap
provider_type: org.keycloak.storage.UserStorageProvider
config:
enabled: true
priority: 0
fullSyncPeriod: -1
changedSyncPeriod: -1
cachePolicy: DEFAULT
batchSizeForSync: 1000
editMode: READ_ONLY
importEnabled: true
syncRegistrations: false
vendor: other
usernameLDAPAttribute: uid
rdnLDAPAttribute: uid
uuidLDAPAttribute: entryUUID
userObjectClasses: "inetOrgPerson, organizationalPerson"
connectionUrl: "ldaps://ldap.example.com:636"
usersDn: "ou=Users,dc=example,dc=com"
authType: simple
bindDn: cn=directory reader
bindCredential: "**********"
searchScope: 1
validatePasswordPolicy: false
trustEmail: false
useTruststoreSpi: "ldapsOnly"
connectionPooling: true
pagination: true
allowKerberosAuthentication: false
useKerberosForPasswordAuthentication: false
debug: false
mappers:
- name: "full name"
providerId: "full-name-ldap-mapper"
providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
config:
ldap.full.name.attribute: cn
read.only: true
write.only: false
register: result
- name: Debug
debug:
var: result
- name: Assert user federation created
assert:
that:
- result is changed
- result.existing != {}
- result.existing.name == "{{ federation }}"
- result.end_state != {}
- result.end_state.name == "{{ federation }}"
- name: Delete existing user federation
community.general.keycloak_user_federation:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ federation }}"
state: absent
register: result
- name: Debug
debug:
var: result
- name: Assert user federation deleted
assert:
that:
- result is changed
- result.existing != {}
- result.end_state == {}
- name: Delete absent user federation
community.general.keycloak_user_federation:
auth_keycloak_url: "{{ url }}"
auth_realm: "{{ admin_realm }}"
auth_username: "{{ admin_user }}"
auth_password: "{{ admin_password }}"
realm: "{{ realm }}"
name: "{{ federation }}"
state: absent
register: result
- name: Debug
debug:
var: result
- name: Assert user federation unchanged
assert:
that:
- result is not changed
- result.existing == {}
- result.end_state == {}

View file

@ -0,0 +1,7 @@
---
url: http://localhost:8080/auth
admin_realm: master
admin_user: admin
admin_password: password
realm: myrealm
federation: myfed

View file

@ -0,0 +1,581 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Ansible Project
# 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
from contextlib import contextmanager
from ansible_collections.community.general.tests.unit.compat import unittest
from ansible_collections.community.general.tests.unit.compat.mock import call, patch
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
from ansible_collections.community.general.plugins.modules.identity.keycloak import keycloak_user_federation
from itertools import count
from ansible.module_utils.six import StringIO
@contextmanager
def patch_keycloak_api(get_components=None, get_component=None, create_component=None, update_component=None, delete_component=None):
"""Mock context manager for patching the methods in KeycloakAPI
"""
obj = keycloak_user_federation.KeycloakAPI
with patch.object(obj, 'get_components', side_effect=get_components) \
as mock_get_components:
with patch.object(obj, 'get_component', side_effect=get_component) \
as mock_get_component:
with patch.object(obj, 'create_component', side_effect=create_component) \
as mock_create_component:
with patch.object(obj, 'update_component', side_effect=update_component) \
as mock_update_component:
with patch.object(obj, 'delete_component', side_effect=delete_component) \
as mock_delete_component:
yield mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component
def get_response(object_with_future_response, method, get_id_call_count):
if callable(object_with_future_response):
return object_with_future_response()
if isinstance(object_with_future_response, dict):
return get_response(
object_with_future_response[method], method, get_id_call_count)
if isinstance(object_with_future_response, list):
call_number = next(get_id_call_count)
return get_response(
object_with_future_response[call_number], method, get_id_call_count)
return object_with_future_response
def build_mocked_request(get_id_user_count, response_dict):
def _mocked_requests(*args, **kwargs):
url = args[0]
method = kwargs['method']
future_response = response_dict.get(url, None)
return get_response(future_response, method, get_id_user_count)
return _mocked_requests
def create_wrapper(text_as_string):
"""Allow to mock many times a call to one address.
Without this function, the StringIO is empty for the second call.
"""
def _create_wrapper():
return StringIO(text_as_string)
return _create_wrapper
def mock_good_connection():
token_response = {
'http://keycloak.url/auth/realms/master/protocol/openid-connect/token': create_wrapper('{"access_token": "alongtoken"}'), }
return patch(
'ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak.open_url',
side_effect=build_mocked_request(count(), token_response),
autospec=True
)
class TestKeycloakUserFederation(ModuleTestCase):
def setUp(self):
super(TestKeycloakUserFederation, self).setUp()
self.module = keycloak_user_federation
def test_create_when_absent(self):
"""Add a new user federation"""
module_args = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'auth_realm': 'master',
'auth_username': 'admin',
'auth_password': 'admin',
'realm': 'realm-name',
'name': 'kerberos',
'state': 'present',
'provider_id': 'kerberos',
'provider_type': 'org.keycloak.storage.UserStorageProvider',
'config': {
'priority': 0,
'enabled': True,
'cachePolicy': 'DEFAULT',
'kerberosRealm': 'REALM',
'serverPrincipal': 'princ',
'keyTab': 'keytab',
'allowPasswordAuthentication': False,
'updateProfileFirstLogin': False,
},
}
return_value_component_create = [
{
"id": "ebb7d999-60cc-4dfe-ab79-48f7bbd9d4d9",
"name": "kerberos",
"providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "kerberos",
"config": {
"serverPrincipal": [
"princ"
],
"allowPasswordAuthentication": [
"false"
],
"keyTab": [
"keytab"
],
"cachePolicy": [
"DEFAULT"
],
"updateProfileFirstLogin": [
"false"
],
"kerberosRealm": [
"REALM"
],
"priority": [
"0"
],
"enabled": [
"true"
]
}
}
]
return_value_components_get = [
[], []
]
changed = True
set_module_args(module_args)
# Run the module
with mock_good_connection():
with patch_keycloak_api(get_components=return_value_components_get, create_component=return_value_component_create) \
as (mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(len(mock_get_components.mock_calls), 1)
self.assertEqual(len(mock_get_component.mock_calls), 0)
self.assertEqual(len(mock_create_component.mock_calls), 1)
self.assertEqual(len(mock_update_component.mock_calls), 0)
self.assertEqual(len(mock_delete_component.mock_calls), 0)
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_create_when_present(self):
"""Update existing user federation"""
module_args = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'auth_realm': 'master',
'auth_username': 'admin',
'auth_password': 'admin',
'realm': 'realm-name',
'name': 'kerberos',
'state': 'present',
'provider_id': 'kerberos',
'provider_type': 'org.keycloak.storage.UserStorageProvider',
'config': {
'priority': 0,
'enabled': True,
'cachePolicy': 'DEFAULT',
'kerberosRealm': 'REALM',
'serverPrincipal': 'princ',
'keyTab': 'keytab',
'allowPasswordAuthentication': False,
'updateProfileFirstLogin': False,
},
}
return_value_components_get = [
[
{
"id": "ebb7d999-60cc-4dfe-ab79-48f7bbd9d4d9",
"name": "kerberos",
"providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "kerberos",
"config": {
"serverPrincipal": [
"princ"
],
"allowPasswordAuthentication": [
"false"
],
"keyTab": [
"keytab"
],
"cachePolicy": [
"DEFAULT"
],
"updateProfileFirstLogin": [
"false"
],
"kerberosRealm": [
"REALM"
],
"priority": [
"0"
],
"enabled": [
"false"
]
}
}
],
[]
]
return_value_component_get = [
{
"id": "ebb7d999-60cc-4dfe-ab79-48f7bbd9d4d9",
"name": "kerberos",
"providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "kerberos",
"config": {
"serverPrincipal": [
"princ"
],
"allowPasswordAuthentication": [
"false"
],
"keyTab": [
"keytab"
],
"cachePolicy": [
"DEFAULT"
],
"updateProfileFirstLogin": [
"false"
],
"kerberosRealm": [
"REALM"
],
"priority": [
"0"
],
"enabled": [
"true"
]
}
}
]
return_value_component_update = [
None
]
changed = True
set_module_args(module_args)
# Run the module
with mock_good_connection():
with patch_keycloak_api(get_components=return_value_components_get, get_component=return_value_component_get,
update_component=return_value_component_update) \
as (mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(len(mock_get_components.mock_calls), 2)
self.assertEqual(len(mock_get_component.mock_calls), 1)
self.assertEqual(len(mock_create_component.mock_calls), 0)
self.assertEqual(len(mock_update_component.mock_calls), 1)
self.assertEqual(len(mock_delete_component.mock_calls), 0)
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_create_with_mappers(self):
"""Add a new user federation with mappers"""
module_args = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'auth_realm': 'master',
'auth_username': 'admin',
'auth_password': 'admin',
'realm': 'realm-name',
'name': 'ldap',
'state': 'present',
'provider_id': 'ldap',
'provider_type': 'org.keycloak.storage.UserStorageProvider',
'config': {
'priority': 0,
'enabled': True,
'cachePolicy': 'DEFAULT',
'batchSizeForSync': 1000,
'editMode': 'READ_ONLY',
'importEnabled': True,
'syncRegistrations': False,
'vendor': 'other',
'usernameLDAPAttribute': 'uid',
'rdnLDAPAttribute': 'uid',
'uuidLDAPAttribute': 'entryUUID',
'userObjectClasses': 'inetOrgPerson, organizationalPerson',
'connectionUrl': 'ldaps://ldap.example.com:636',
'usersDn': 'ou=Users,dc=example,dc=com',
'authType': 'none',
'searchScope': 1,
'validatePasswordPolicy': False,
'trustEmail': False,
'useTruststoreSpi': 'ldapsOnly',
'connectionPooling': True,
'pagination': True,
'allowKerberosAuthentication': False,
'debug': False,
'useKerberosForPasswordAuthentication': False,
},
'mappers': [
{
'name': 'full name',
'providerId': 'full-name-ldap-mapper',
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
'config': {
'ldap.full.name.attribute': 'cn',
'read.only': True,
'write.only': False,
}
}
]
}
return_value_components_get = [
[]
]
return_value_component_create = [
{
"id": "eb691537-b73c-4cd8-b481-6031c26499d8",
"name": "ldap",
"providerId": "ldap",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "ldap",
"config": {
"pagination": [
"true"
],
"connectionPooling": [
"true"
],
"usersDn": [
"ou=Users,dc=example,dc=com"
],
"cachePolicy": [
"DEFAULT"
],
"useKerberosForPasswordAuthentication": [
"false"
],
"importEnabled": [
"true"
],
"enabled": [
"true"
],
"usernameLDAPAttribute": [
"uid"
],
"vendor": [
"other"
],
"uuidLDAPAttribute": [
"entryUUID"
],
"connectionUrl": [
"ldaps://ldap.example.com:636"
],
"allowKerberosAuthentication": [
"false"
],
"syncRegistrations": [
"false"
],
"authType": [
"none"
],
"debug": [
"false"
],
"searchScope": [
"1"
],
"useTruststoreSpi": [
"ldapsOnly"
],
"trustEmail": [
"false"
],
"priority": [
"0"
],
"userObjectClasses": [
"inetOrgPerson, organizationalPerson"
],
"rdnLDAPAttribute": [
"uid"
],
"editMode": [
"READ_ONLY"
],
"validatePasswordPolicy": [
"false"
],
"batchSizeForSync": [
"1000"
]
}
},
{
"id": "2dfadafd-8b34-495f-a98b-153e71a22311",
"name": "full name",
"providerId": "full-name-ldap-mapper",
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
"parentId": "eb691537-b73c-4cd8-b481-6031c26499d8",
"config": {
"ldap.full.name.attribute": [
"cn"
],
"read.only": [
"true"
],
"write.only": [
"false"
]
}
}
]
changed = True
set_module_args(module_args)
# Run the module
with mock_good_connection():
with patch_keycloak_api(get_components=return_value_components_get, create_component=return_value_component_create) \
as (mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(len(mock_get_components.mock_calls), 1)
self.assertEqual(len(mock_get_component.mock_calls), 0)
self.assertEqual(len(mock_create_component.mock_calls), 2)
self.assertEqual(len(mock_update_component.mock_calls), 0)
self.assertEqual(len(mock_delete_component.mock_calls), 0)
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_delete_when_absent(self):
"""Remove an absent user federation"""
module_args = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'auth_realm': 'master',
'auth_username': 'admin',
'auth_password': 'admin',
'realm': 'realm-name',
'name': 'kerberos',
'state': 'absent',
}
return_value_components_get = [
[]
]
changed = False
set_module_args(module_args)
# Run the module
with mock_good_connection():
with patch_keycloak_api(get_components=return_value_components_get) \
as (mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(len(mock_get_components.mock_calls), 1)
self.assertEqual(len(mock_get_component.mock_calls), 0)
self.assertEqual(len(mock_create_component.mock_calls), 0)
self.assertEqual(len(mock_update_component.mock_calls), 0)
self.assertEqual(len(mock_delete_component.mock_calls), 0)
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_delete_when_present(self):
"""Remove an existing user federation"""
module_args = {
'auth_keycloak_url': 'http://keycloak.url/auth',
'auth_realm': 'master',
'auth_username': 'admin',
'auth_password': 'admin',
'realm': 'realm-name',
'name': 'kerberos',
'state': 'absent',
}
return_value_components_get = [
[
{
"id": "ebb7d999-60cc-4dfe-ab79-48f7bbd9d4d9",
"name": "kerberos",
"providerId": "kerberos",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "kerberos",
"config": {
"serverPrincipal": [
"princ"
],
"allowPasswordAuthentication": [
"false"
],
"keyTab": [
"keytab"
],
"cachePolicy": [
"DEFAULT"
],
"updateProfileFirstLogin": [
"false"
],
"kerberosRealm": [
"REALM"
],
"priority": [
"0"
],
"enabled": [
"false"
]
}
}
],
[]
]
return_value_component_delete = [
None
]
changed = True
set_module_args(module_args)
# Run the module
with mock_good_connection():
with patch_keycloak_api(get_components=return_value_components_get, delete_component=return_value_component_delete) \
as (mock_get_components, mock_get_component, mock_create_component, mock_update_component, mock_delete_component):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
self.assertEqual(len(mock_get_components.mock_calls), 2)
self.assertEqual(len(mock_get_component.mock_calls), 0)
self.assertEqual(len(mock_create_component.mock_calls), 0)
self.assertEqual(len(mock_update_component.mock_calls), 0)
self.assertEqual(len(mock_delete_component.mock_calls), 1)
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
if __name__ == '__main__':
unittest.main()