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:
parent
02d0e3d286
commit
2589e9a030
8 changed files with 1889 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -521,6 +521,8 @@ files:
|
|||
maintainers: kris2kris
|
||||
$modules/identity/keycloak/keycloak_role.py:
|
||||
maintainers: laurpaum
|
||||
$modules/identity/keycloak/keycloak_user_federation.py:
|
||||
maintainers: laurpaum
|
||||
$modules/identity/onepassword_info.py:
|
||||
maintainers: Rylon
|
||||
$modules/identity/opendj/opendj_backendprop.py:
|
||||
|
|
|
@ -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_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():
|
||||
"""
|
||||
|
@ -1601,3 +1604,93 @@ class KeycloakAPI(object):
|
|||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete mapper %s for identity provider %s in realm %s: %s'
|
||||
% (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)))
|
||||
|
|
979
plugins/modules/identity/keycloak/keycloak_user_federation.py
Normal file
979
plugins/modules/identity/keycloak/keycloak_user_federation.py
Normal 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()
|
1
plugins/modules/keycloak_user_federation.py
Symbolic link
1
plugins/modules/keycloak_user_federation.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
identity/keycloak/keycloak_user_federation.py
|
|
@ -0,0 +1 @@
|
|||
unsupported
|
|
@ -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 == {}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
url: http://localhost:8080/auth
|
||||
admin_realm: master
|
||||
admin_user: admin
|
||||
admin_password: password
|
||||
realm: myrealm
|
||||
federation: myfed
|
|
@ -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()
|
Loading…
Reference in a new issue