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