mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
aca2afc6f8
keycloak_user_federation: sort desired and after mappers by name (#8761)
* sort desired mappers by name
* sort mappers fetched after update by name
* only sort mapper list if there are desired mappers specified
* add fallback `''` in case `name` is not a key or `None` when sorting mappers
* add changelog fragment
(cherry picked from commit 982b8d89b7
)
Co-authored-by: fgruenbauer <gruenbauer@b1-systems.de>
1071 lines
42 KiB
Python
1071 lines
42 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (c) Ansible project
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
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/20.0.2/rest-api/index.html).
|
|
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: full
|
|
|
|
options:
|
|
state:
|
|
description:
|
|
- State of the user federation.
|
|
- On V(present), the user federation will be created if it does not yet exist, or updated with
|
|
the parameters you provide.
|
|
- On V(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 O(name).
|
|
type: str
|
|
|
|
name:
|
|
description:
|
|
- Display name of provider when linked in admin console.
|
|
type: str
|
|
|
|
provider_id:
|
|
description:
|
|
- Provider for this user federation. Built-in providers are V(ldap), V(kerberos), and V(sssd).
|
|
Custom user storage providers can also be used.
|
|
aliases:
|
|
- providerId
|
|
type: str
|
|
|
|
provider_type:
|
|
description:
|
|
- Component type for user federation (only supported value is V(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
|
|
|
|
remove_unspecified_mappers:
|
|
description:
|
|
- Remove mappers that are not specified in the configuration for this federation.
|
|
- Set to V(false) to keep mappers that are not listed in O(mappers).
|
|
type: bool
|
|
default: true
|
|
version_added: 9.4.0
|
|
|
|
config:
|
|
description:
|
|
- Dict specifying the configuration options for the provider; the contents differ depending on
|
|
the value of O(provider_id). Examples are given below for V(ldap), V(kerberos) and V(sssd).
|
|
It is easiest to obtain valid config values by dumping an already-existing user federation
|
|
configuration through check-mode in the RV(existing) field.
|
|
- The value V(sssd) has been supported since community.general 4.2.0.
|
|
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 V(true), LDAP users will be imported into Keycloak DB and synced by the configured
|
|
sync policies.
|
|
default: true
|
|
type: bool
|
|
|
|
editMode:
|
|
description:
|
|
- V(READ_ONLY) is a read-only LDAP store. V(WRITABLE) means data will be synced back to LDAP
|
|
on demand. V(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).
|
|
- Use short name. For instance, write V(rhds) for "Red Hat Directory Server".
|
|
type: str
|
|
|
|
usernameLDAPAttribute:
|
|
description:
|
|
- Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server
|
|
vendors it can be V(uid). For Active directory it can be V(sAMAccountName) or V(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 V(cn) as RDN attribute when
|
|
username attribute might be V(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 V(entryUUID); however some are different.
|
|
For example for Active directory it should be V(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 V(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. V(always) means that it will always use it.
|
|
V(never) means that it will not use it. V(ldapsOnly) 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
|
|
V(fine) (trace connection creation and removal) and V(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 V(plain) and V(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
|
|
|
|
krbPrincipalAttribute:
|
|
description:
|
|
- Name of the LDAP attribute, which refers to Kerberos principal.
|
|
This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak.
|
|
When this is empty, the LDAP user will be looked based on LDAP username corresponding
|
|
to the first part of his Kerberos principal. For instance, for principal C(john@KEYCLOAK.ORG),
|
|
it will assume that LDAP username is V(john).
|
|
type: str
|
|
version_added: 8.1.0
|
|
|
|
serverPrincipal:
|
|
description:
|
|
- Full name of server principal for HTTP service including server and domain name. For
|
|
example V(HTTP/host.foo.org@FOO.ORG). Use V(*) 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 V(/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 V(user-attribute-ldap-mapper)).
|
|
type: str
|
|
|
|
providerType:
|
|
description:
|
|
- Component type for this mapper.
|
|
type: str
|
|
default: org.keycloak.storage.ldap.mappers.LDAPStorageMapper
|
|
|
|
config:
|
|
description:
|
|
- Dict specifying the configuration options for the mapper; the contents differ
|
|
depending on the value of I(identityProviderMapper).
|
|
# TODO: what is identityProviderMapper above???
|
|
type: dict
|
|
|
|
extends_documentation_fragment:
|
|
- community.general.keycloak
|
|
- community.general.attributes
|
|
|
|
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: Create sssd 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-sssd
|
|
state: present
|
|
provider_id: sssd
|
|
provider_type: org.keycloak.storage.UserStorageProvider
|
|
config:
|
|
priority: 0
|
|
enabled: true
|
|
cachePolicy: DEFAULT
|
|
|
|
- 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 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: on success
|
|
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'] = {k: v[0] for k, v in compcopy['config'].items()}
|
|
if 'bindCredential' in compcopy['config']:
|
|
compcopy['config']['bindCredential'] = '**********'
|
|
# an empty string is valid for krbPrincipalAttribute but is filtered out in diff
|
|
if 'krbPrincipalAttribute' not in compcopy['config']:
|
|
compcopy['config']['krbPrincipalAttribute'] = ''
|
|
if 'mappers' in compcopy:
|
|
for mapper in compcopy['mappers']:
|
|
if 'config' in mapper:
|
|
mapper['config'] = {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'),
|
|
krbPrincipalAttribute=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', default='org.keycloak.storage.ldap.mappers.LDAPStorageMapper'),
|
|
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']),
|
|
provider_type=dict(type='str', aliases=['providerType'], default='org.keycloak.storage.UserStorageProvider'),
|
|
parent_id=dict(type='str', aliases=['parentId']),
|
|
remove_unspecified_mappers=dict(type='bool', default=True),
|
|
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'] = {
|
|
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)
|
|
|
|
# Filter and map the parameters names that apply
|
|
comp_params = [x for x in module.params
|
|
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'mappers', 'remove_unspecified_mappers'] and
|
|
module.params.get(x) is not None]
|
|
|
|
# See if it already exists in Keycloak
|
|
if cid is None:
|
|
found = kc.get_components(urlencode(dict(type='org.keycloak.storage.UserStorageProvider', 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 = {}
|
|
|
|
# if user federation exists, get associated mappers
|
|
if cid is not None and before_comp:
|
|
before_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '')
|
|
|
|
# Build a proposed changeset from parameters given to this module
|
|
changeset = {}
|
|
|
|
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 = [{k: v for k, v in x.items() if v 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'] in ['kerberos', 'sssd']:
|
|
module.fail_json(msg='Cannot configure mappers for {type} provider.'.format(type=module.params['provider_id']))
|
|
for change in module.params['mappers']:
|
|
change = {k: v for k, v in change.items() if v 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 = {}
|
|
elif change.get('id') is not None:
|
|
old_mapper = next((before_mapper for before_mapper in before_mapper.get('mappers', []) if before_mapper["id"] == change['id']), None)
|
|
if old_mapper is None:
|
|
old_mapper = {}
|
|
else:
|
|
found = [before_mapper for before_mapper in before_comp.get('mappers', []) if before_mapper['name'] == change['name']]
|
|
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 = {}
|
|
new_mapper = old_mapper.copy()
|
|
new_mapper.update(change)
|
|
# changeset contains all desired mappers: those existing, to update or to create
|
|
if changeset.get('mappers') is None:
|
|
changeset['mappers'] = list()
|
|
changeset['mappers'].append(new_mapper)
|
|
changeset['mappers'] = sorted(changeset['mappers'], key=lambda x: x.get('name') or '')
|
|
|
|
# to keep unspecified existing mappers we add them to the desired mappers list, unless they're already present
|
|
if not module.params['remove_unspecified_mappers'] and 'mappers' in before_comp:
|
|
changeset_mapper_ids = [mapper['id'] for mapper in changeset['mappers'] if 'id' in mapper]
|
|
changeset['mappers'].extend([mapper for mapper in before_comp['mappers'] if mapper['id'] not in changeset_mapper_ids])
|
|
|
|
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
|
desired_comp = before_comp.copy()
|
|
desired_comp.update(changeset)
|
|
|
|
result['proposed'] = sanitize(changeset)
|
|
result['existing'] = sanitize(before_comp)
|
|
|
|
# Cater for when it doesn't exist (an empty dict)
|
|
if not before_comp:
|
|
if state == 'absent':
|
|
# Do nothing and exit
|
|
if module._diff:
|
|
result['diff'] = dict(before='', after='')
|
|
result['changed'] = False
|
|
result['end_state'] = {}
|
|
result['msg'] = 'User federation does not exist; doing nothing.'
|
|
module.exit_json(**result)
|
|
|
|
# Process a creation
|
|
result['changed'] = True
|
|
|
|
if module.check_mode:
|
|
if module._diff:
|
|
result['diff'] = dict(before='', after=sanitize(desired_comp))
|
|
module.exit_json(**result)
|
|
|
|
# create it
|
|
desired_mappers = desired_comp.pop('mappers', [])
|
|
after_comp = kc.create_component(desired_comp, realm)
|
|
cid = after_comp['id']
|
|
updated_mappers = []
|
|
# when creating a user federation, keycloak automatically creates default mappers
|
|
default_mappers = kc.get_components(urlencode(dict(parent=cid)), realm)
|
|
|
|
# create new mappers or update existing default mappers
|
|
for desired_mapper in desired_mappers:
|
|
found = [default_mapper for default_mapper in default_mappers if default_mapper['name'] == desired_mapper['name']]
|
|
if len(found) > 1:
|
|
module.fail_json(msg='Found multiple mappers with name `{name}`. Cannot continue.'.format(name=desired_mapper['name']))
|
|
if len(found) == 1:
|
|
old_mapper = found[0]
|
|
else:
|
|
old_mapper = {}
|
|
|
|
new_mapper = old_mapper.copy()
|
|
new_mapper.update(desired_mapper)
|
|
|
|
if new_mapper.get('id') is not None:
|
|
kc.update_component(new_mapper, realm)
|
|
updated_mappers.append(new_mapper)
|
|
else:
|
|
if new_mapper.get('parentId') is None:
|
|
new_mapper['parentId'] = cid
|
|
updated_mappers.append(kc.create_component(new_mapper, realm))
|
|
|
|
if module.params['remove_unspecified_mappers']:
|
|
# we remove all unwanted default mappers
|
|
# we use ids so we dont accidently remove one of the previously updated default mapper
|
|
for default_mapper in default_mappers:
|
|
if not default_mapper['id'] in [x['id'] for x in updated_mappers]:
|
|
kc.delete_component(default_mapper['id'], realm)
|
|
|
|
after_comp['mappers'] = kc.get_components(urlencode(dict(parent=cid)), realm)
|
|
if module._diff:
|
|
result['diff'] = dict(before='', after=sanitize(after_comp))
|
|
result['end_state'] = sanitize(after_comp)
|
|
result['msg'] = "User federation {id} has been created".format(id=cid)
|
|
module.exit_json(**result)
|
|
|
|
else:
|
|
if state == 'present':
|
|
# Process an update
|
|
|
|
# no changes
|
|
if desired_comp == before_comp:
|
|
result['changed'] = False
|
|
result['end_state'] = sanitize(desired_comp)
|
|
result['msg'] = "No changes required to user federation {id}.".format(id=cid)
|
|
module.exit_json(**result)
|
|
|
|
# doing an update
|
|
result['changed'] = True
|
|
|
|
if module._diff:
|
|
result['diff'] = dict(before=sanitize(before_comp), after=sanitize(desired_comp))
|
|
|
|
if module.check_mode:
|
|
module.exit_json(**result)
|
|
|
|
# do the update
|
|
desired_mappers = desired_comp.pop('mappers', [])
|
|
kc.update_component(desired_comp, realm)
|
|
|
|
for before_mapper in before_comp.get('mappers', []):
|
|
# remove unwanted existing mappers that will not be updated
|
|
if not before_mapper['id'] in [x['id'] for x in desired_mappers if 'id' in x]:
|
|
kc.delete_component(before_mapper['id'], realm)
|
|
|
|
for mapper in desired_mappers:
|
|
if mapper in before_comp.get('mappers', []):
|
|
continue
|
|
if mapper.get('id') is not None:
|
|
kc.update_component(mapper, realm)
|
|
else:
|
|
if mapper.get('parentId') is None:
|
|
mapper['parentId'] = desired_comp['id']
|
|
kc.create_component(mapper, realm)
|
|
|
|
after_comp = kc.get_component(cid, realm)
|
|
after_comp['mappers'] = sorted(kc.get_components(urlencode(dict(parent=cid)), realm), key=lambda x: x.get('name') or '')
|
|
after_comp_sanitized = sanitize(after_comp)
|
|
before_comp_sanitized = sanitize(before_comp)
|
|
result['end_state'] = after_comp_sanitized
|
|
if module._diff:
|
|
result['diff'] = dict(before=before_comp_sanitized, after=after_comp_sanitized)
|
|
result['changed'] = before_comp_sanitized != after_comp_sanitized
|
|
result['msg'] = "User federation {id} has been updated".format(id=cid)
|
|
module.exit_json(**result)
|
|
|
|
elif state == 'absent':
|
|
# Process a deletion
|
|
result['changed'] = True
|
|
|
|
if module._diff:
|
|
result['diff'] = dict(before=sanitize(before_comp), after='')
|
|
|
|
if module.check_mode:
|
|
module.exit_json(**result)
|
|
|
|
# delete it
|
|
kc.delete_component(cid, realm)
|
|
|
|
result['end_state'] = {}
|
|
|
|
result['msg'] = "User federation {id} has been deleted".format(id=cid)
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|