2023-06-09 12:56:06 +02:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Copyright (c) 2019, INSPQ (@elfelip)
|
|
|
|
# GNU General Public License v3.0+ (see COPYING 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
|
|
|
|
short_description: Create and configure a user in Keycloak
|
|
|
|
description:
|
|
|
|
- This module creates, removes, or updates Keycloak users.
|
|
|
|
version_added: 7.1.0
|
|
|
|
options:
|
|
|
|
auth_username:
|
|
|
|
aliases: []
|
|
|
|
realm:
|
|
|
|
description:
|
|
|
|
- The name of the realm in which is the client.
|
|
|
|
default: master
|
|
|
|
type: str
|
|
|
|
username:
|
|
|
|
description:
|
|
|
|
- Username for the user.
|
|
|
|
required: true
|
|
|
|
type: str
|
|
|
|
id:
|
|
|
|
description:
|
|
|
|
- ID of the user on the Keycloak server if known.
|
|
|
|
type: str
|
|
|
|
enabled:
|
|
|
|
description:
|
|
|
|
- Enabled user.
|
|
|
|
type: bool
|
|
|
|
email_verified:
|
|
|
|
description:
|
|
|
|
- Check the validity of user email.
|
|
|
|
default: false
|
|
|
|
type: bool
|
|
|
|
aliases:
|
|
|
|
- emailVerified
|
|
|
|
first_name:
|
|
|
|
description:
|
|
|
|
- The user's first name.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
aliases:
|
|
|
|
- firstName
|
|
|
|
last_name:
|
|
|
|
description:
|
|
|
|
- The user's last name.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
aliases:
|
|
|
|
- lastName
|
|
|
|
email:
|
|
|
|
description:
|
|
|
|
- User email.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
federation_link:
|
|
|
|
description:
|
|
|
|
- Federation Link.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
aliases:
|
|
|
|
- federationLink
|
|
|
|
service_account_client_id:
|
|
|
|
description:
|
|
|
|
- Description of the client Application.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
aliases:
|
|
|
|
- serviceAccountClientId
|
|
|
|
client_consents:
|
|
|
|
description:
|
|
|
|
- Client Authenticator Type.
|
|
|
|
type: list
|
|
|
|
elements: dict
|
|
|
|
default: []
|
|
|
|
aliases:
|
|
|
|
- clientConsents
|
|
|
|
suboptions:
|
|
|
|
client_id:
|
|
|
|
description:
|
|
|
|
- Client ID of the client role. Not the technical ID of the client.
|
|
|
|
type: str
|
|
|
|
required: true
|
|
|
|
aliases:
|
|
|
|
- clientId
|
|
|
|
roles:
|
|
|
|
description:
|
|
|
|
- List of client roles to assign to the user.
|
|
|
|
type: list
|
|
|
|
required: true
|
|
|
|
elements: str
|
|
|
|
groups:
|
|
|
|
description:
|
|
|
|
- List of groups for the user.
|
|
|
|
type: list
|
|
|
|
elements: dict
|
|
|
|
default: []
|
|
|
|
suboptions:
|
|
|
|
name:
|
|
|
|
description:
|
|
|
|
- Name of the group.
|
|
|
|
type: str
|
|
|
|
state:
|
|
|
|
description:
|
|
|
|
- Control whether the user must be member of this group or not.
|
|
|
|
choices: [ "present", "absent" ]
|
|
|
|
default: present
|
|
|
|
type: str
|
|
|
|
credentials:
|
|
|
|
description:
|
|
|
|
- User credentials.
|
|
|
|
default: []
|
|
|
|
type: list
|
|
|
|
elements: dict
|
|
|
|
suboptions:
|
|
|
|
type:
|
|
|
|
description:
|
|
|
|
- Credential type.
|
|
|
|
type: str
|
|
|
|
required: true
|
|
|
|
value:
|
|
|
|
description:
|
|
|
|
- Value of the credential.
|
|
|
|
type: str
|
|
|
|
required: true
|
|
|
|
temporary:
|
|
|
|
description:
|
2023-06-15 19:04:45 +02:00
|
|
|
- If V(true), the users are required to reset their credentials at next login.
|
2023-06-09 12:56:06 +02:00
|
|
|
type: bool
|
|
|
|
default: false
|
|
|
|
required_actions:
|
|
|
|
description:
|
|
|
|
- RequiredActions user Auth.
|
|
|
|
default: []
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
aliases:
|
|
|
|
- requiredActions
|
|
|
|
federated_identities:
|
|
|
|
description:
|
|
|
|
- List of IDPs of user.
|
|
|
|
default: []
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
aliases:
|
|
|
|
- federatedIdentities
|
|
|
|
attributes:
|
|
|
|
description:
|
|
|
|
- List of user attributes.
|
|
|
|
required: false
|
|
|
|
type: list
|
|
|
|
elements: dict
|
|
|
|
suboptions:
|
|
|
|
name:
|
|
|
|
description:
|
|
|
|
- Name of the attribute.
|
|
|
|
type: str
|
|
|
|
values:
|
|
|
|
description:
|
|
|
|
- Values for the attribute as list.
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
state:
|
|
|
|
description:
|
|
|
|
- Control whether the attribute must exists or not.
|
|
|
|
choices: [ "present", "absent" ]
|
|
|
|
default: present
|
|
|
|
type: str
|
|
|
|
access:
|
|
|
|
description:
|
|
|
|
- list user access.
|
|
|
|
required: false
|
|
|
|
type: dict
|
|
|
|
disableable_credential_types:
|
|
|
|
description:
|
|
|
|
- list user Credential Type.
|
|
|
|
default: []
|
|
|
|
type: list
|
|
|
|
elements: str
|
|
|
|
aliases:
|
|
|
|
- disableableCredentialTypes
|
|
|
|
origin:
|
|
|
|
description:
|
|
|
|
- user origin.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
self:
|
|
|
|
description:
|
|
|
|
- user self administration.
|
|
|
|
required: false
|
|
|
|
type: str
|
|
|
|
state:
|
|
|
|
description:
|
|
|
|
- Control whether the user should exists or not.
|
|
|
|
choices: [ "present", "absent" ]
|
|
|
|
default: present
|
|
|
|
type: str
|
|
|
|
force:
|
|
|
|
description:
|
2023-06-15 19:04:45 +02:00
|
|
|
- If V(true), allows to remove user and recreate it.
|
2023-06-09 12:56:06 +02:00
|
|
|
type: bool
|
|
|
|
default: false
|
|
|
|
extends_documentation_fragment:
|
|
|
|
- community.general.keycloak
|
|
|
|
- community.general.attributes
|
|
|
|
attributes:
|
|
|
|
check_mode:
|
|
|
|
support: full
|
|
|
|
diff_mode:
|
|
|
|
support: full
|
|
|
|
notes:
|
|
|
|
- The module does not modify the user ID of an existing user.
|
|
|
|
author:
|
|
|
|
- Philippe Gauthier (@elfelip)
|
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = '''
|
|
|
|
- name: Create a user user1
|
|
|
|
community.general.keycloak_user:
|
|
|
|
auth_keycloak_url: http://localhost:8080/auth
|
|
|
|
auth_username: admin
|
|
|
|
auth_password: password
|
|
|
|
realm: master
|
|
|
|
username: user1
|
|
|
|
firstName: user1
|
|
|
|
lastName: user1
|
|
|
|
email: user1
|
|
|
|
enabled: true
|
|
|
|
emailVerified: false
|
|
|
|
credentials:
|
|
|
|
- type: password
|
|
|
|
value: password
|
|
|
|
temporary: false
|
|
|
|
attributes:
|
|
|
|
- name: attr1
|
|
|
|
values:
|
|
|
|
- value1
|
|
|
|
state: present
|
|
|
|
- name: attr2
|
|
|
|
values:
|
|
|
|
- value2
|
|
|
|
state: absent
|
|
|
|
groups:
|
|
|
|
- name: group1
|
|
|
|
state: present
|
|
|
|
state: present
|
|
|
|
|
|
|
|
- name: Re-create a User
|
|
|
|
community.general.keycloak_user:
|
|
|
|
auth_keycloak_url: http://localhost:8080/auth
|
|
|
|
auth_username: admin
|
|
|
|
auth_password: password
|
|
|
|
realm: master
|
|
|
|
username: user1
|
|
|
|
firstName: user1
|
|
|
|
lastName: user1
|
|
|
|
email: user1
|
|
|
|
enabled: true
|
|
|
|
emailVerified: false
|
|
|
|
credentials:
|
|
|
|
- type: password
|
|
|
|
value: password
|
|
|
|
temporary: false
|
|
|
|
attributes:
|
|
|
|
- name: attr1
|
|
|
|
values:
|
|
|
|
- value1
|
|
|
|
state: present
|
|
|
|
- name: attr2
|
|
|
|
values:
|
|
|
|
- value2
|
|
|
|
state: absent
|
|
|
|
groups:
|
|
|
|
- name: group1
|
|
|
|
state: present
|
|
|
|
state: present
|
|
|
|
|
|
|
|
- name: Re-create a User
|
|
|
|
community.general.keycloak_user:
|
|
|
|
auth_keycloak_url: http://localhost:8080/auth
|
|
|
|
auth_username: admin
|
|
|
|
auth_password: password
|
|
|
|
realm: master
|
|
|
|
username: user1
|
|
|
|
firstName: user1
|
|
|
|
lastName: user1
|
|
|
|
email: user1
|
|
|
|
enabled: true
|
|
|
|
emailVerified: false
|
|
|
|
credentials:
|
|
|
|
- type: password
|
|
|
|
value: password
|
|
|
|
temporary: false
|
|
|
|
attributes:
|
|
|
|
- name: attr1
|
|
|
|
values:
|
|
|
|
- value1
|
|
|
|
state: present
|
|
|
|
- name: attr2
|
|
|
|
values:
|
|
|
|
- value2
|
|
|
|
state: absent
|
|
|
|
groups:
|
|
|
|
- name: group1
|
|
|
|
state: present
|
|
|
|
state: present
|
|
|
|
force: true
|
|
|
|
|
|
|
|
- name: Remove User
|
|
|
|
community.general.keycloak_user:
|
|
|
|
auth_keycloak_url: http://localhost:8080/auth
|
|
|
|
auth_username: admin
|
|
|
|
auth_password: password
|
|
|
|
realm: master
|
|
|
|
username: user1
|
|
|
|
state: absent
|
|
|
|
'''
|
|
|
|
|
|
|
|
RETURN = '''
|
|
|
|
msg:
|
|
|
|
description: Message as to what action was taken.
|
|
|
|
returned: always
|
|
|
|
type: str
|
|
|
|
sample: User f18c709c-03d6-11ee-970b-c74bf2721112 created
|
|
|
|
proposed:
|
|
|
|
description: Representation of the proposed user.
|
|
|
|
returned: on success
|
|
|
|
type: dict
|
|
|
|
existing:
|
|
|
|
description: Representation of the existing user.
|
|
|
|
returned: on success
|
|
|
|
type: dict
|
|
|
|
end_state:
|
|
|
|
description: Representation of the user after module execution
|
|
|
|
returned: on success
|
|
|
|
type: dict
|
|
|
|
changed:
|
2023-06-15 19:04:45 +02:00
|
|
|
description: Return V(true) if the operation changed the user on the keycloak server, V(false) otherwise.
|
2023-06-09 12:56:06 +02:00
|
|
|
returned: always
|
|
|
|
type: bool
|
|
|
|
'''
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \
|
|
|
|
keycloak_argument_spec, get_token, KeycloakError, is_struct_included
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
import copy
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
argument_spec = keycloak_argument_spec()
|
|
|
|
argument_spec['auth_username']['aliases'] = []
|
|
|
|
credential_spec = dict(
|
|
|
|
type=dict(type='str', required=True),
|
|
|
|
value=dict(type='str', required=True),
|
|
|
|
temporary=dict(type='bool', default=False)
|
|
|
|
)
|
|
|
|
client_consents_spec = dict(
|
|
|
|
client_id=dict(type='str', required=True, aliases=['clientId']),
|
|
|
|
roles=dict(type='list', elements='str', required=True)
|
|
|
|
)
|
|
|
|
attributes_spec = dict(
|
|
|
|
name=dict(type='str'),
|
|
|
|
values=dict(type='list', elements='str'),
|
|
|
|
state=dict(type='str', choices=['present', 'absent'], default='present')
|
|
|
|
)
|
|
|
|
groups_spec = dict(
|
|
|
|
name=dict(type='str'),
|
|
|
|
state=dict(type='str', choices=['present', 'absent'], default='present')
|
|
|
|
)
|
|
|
|
meta_args = dict(
|
|
|
|
realm=dict(type='str', default='master'),
|
|
|
|
self=dict(type='str'),
|
|
|
|
id=dict(type='str'),
|
|
|
|
username=dict(type='str', required=True),
|
|
|
|
first_name=dict(type='str', aliases=['firstName']),
|
|
|
|
last_name=dict(type='str', aliases=['lastName']),
|
|
|
|
email=dict(type='str'),
|
|
|
|
enabled=dict(type='bool'),
|
|
|
|
email_verified=dict(type='bool', default=False, aliases=['emailVerified']),
|
|
|
|
federation_link=dict(type='str', aliases=['federationLink']),
|
|
|
|
service_account_client_id=dict(type='str', aliases=['serviceAccountClientId']),
|
|
|
|
attributes=dict(type='list', elements='dict', options=attributes_spec),
|
|
|
|
access=dict(type='dict'),
|
|
|
|
groups=dict(type='list', default=[], elements='dict', options=groups_spec),
|
|
|
|
disableable_credential_types=dict(type='list', default=[], aliases=['disableableCredentialTypes'], elements='str'),
|
|
|
|
required_actions=dict(type='list', default=[], aliases=['requiredActions'], elements='str'),
|
|
|
|
credentials=dict(type='list', default=[], elements='dict', options=credential_spec),
|
|
|
|
federated_identities=dict(type='list', default=[], aliases=['federatedIdentities'], elements='str'),
|
|
|
|
client_consents=dict(type='list', default=[], aliases=['clientConsents'], elements='dict', options=client_consents_spec),
|
|
|
|
origin=dict(type='str'),
|
|
|
|
state=dict(choices=["absent", "present"], default='present'),
|
|
|
|
force=dict(type='bool', default=False),
|
|
|
|
)
|
|
|
|
argument_spec.update(meta_args)
|
|
|
|
|
|
|
|
module = AnsibleModule(argument_spec=argument_spec,
|
|
|
|
supports_check_mode=True,
|
|
|
|
required_one_of=([['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')
|
|
|
|
force = module.params.get('force')
|
|
|
|
username = module.params.get('username')
|
|
|
|
groups = module.params.get('groups')
|
|
|
|
|
|
|
|
# Filter and map the parameters names that apply to the user
|
|
|
|
user_params = [x for x in module.params
|
|
|
|
if x not in list(keycloak_argument_spec().keys()) + ['state', 'realm', 'force', 'groups'] and
|
|
|
|
module.params.get(x) is not None]
|
|
|
|
|
|
|
|
before_user = kc.get_user_by_username(username=username, realm=realm)
|
|
|
|
|
|
|
|
if before_user is None:
|
|
|
|
before_user = {}
|
|
|
|
|
|
|
|
changeset = {}
|
|
|
|
|
|
|
|
for param in user_params:
|
|
|
|
new_param_value = module.params.get(param)
|
|
|
|
if param == 'attributes' and param in before_user:
|
|
|
|
old_value = kc.convert_keycloak_user_attributes_dict_to_module_list(attributes=before_user['attributes'])
|
|
|
|
else:
|
|
|
|
old_value = before_user[param] if param in before_user else None
|
|
|
|
if new_param_value != old_value:
|
|
|
|
if old_value is not None and param == 'attributes':
|
|
|
|
for old_attribute in old_value:
|
|
|
|
old_attribute_found = False
|
|
|
|
for new_attribute in new_param_value:
|
|
|
|
if new_attribute['name'] == old_attribute['name']:
|
|
|
|
old_attribute_found = True
|
|
|
|
if not old_attribute_found:
|
|
|
|
new_param_value.append(copy.deepcopy(old_attribute))
|
|
|
|
if isinstance(new_param_value, dict):
|
|
|
|
changeset[camel(param)] = copy.deepcopy(new_param_value)
|
|
|
|
else:
|
|
|
|
changeset[camel(param)] = new_param_value
|
|
|
|
# Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis)
|
|
|
|
desired_user = copy.deepcopy(before_user)
|
|
|
|
desired_user.update(changeset)
|
|
|
|
|
|
|
|
result['proposed'] = changeset
|
|
|
|
result['existing'] = before_user
|
|
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
# Cater for when it doesn't exist (an empty dict)
|
|
|
|
if state == 'absent':
|
|
|
|
if not before_user:
|
|
|
|
# Do nothing and exit
|
|
|
|
if module._diff:
|
|
|
|
result['diff'] = dict(before='', after='')
|
|
|
|
result['changed'] = False
|
|
|
|
result['end_state'] = {}
|
|
|
|
result['msg'] = 'Role does not exist, doing nothing.'
|
|
|
|
module.exit_json(**result)
|
|
|
|
else:
|
|
|
|
# Delete user
|
|
|
|
kc.delete_user(user_id=before_user['id'], realm=realm)
|
|
|
|
result["msg"] = 'User %s deleted' % (before_user['username'])
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
else:
|
|
|
|
after_user = {}
|
2023-12-21 13:20:29 +01:00
|
|
|
if force and before_user: # If the force option is set to true
|
2023-06-09 12:56:06 +02:00
|
|
|
# Delete the existing user
|
|
|
|
kc.delete_user(user_id=before_user["id"], realm=realm)
|
|
|
|
|
|
|
|
if not before_user or force:
|
|
|
|
# Process a creation
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if username is None:
|
|
|
|
module.fail_json(msg='username must be specified when creating a new user')
|
|
|
|
|
|
|
|
if module._diff:
|
|
|
|
result['diff'] = dict(before='', after=desired_user)
|
|
|
|
|
|
|
|
if module.check_mode:
|
|
|
|
module.exit_json(**result)
|
|
|
|
# Create the user
|
|
|
|
after_user = kc.create_user(userrep=desired_user, realm=realm)
|
|
|
|
result["msg"] = 'User %s created' % (desired_user['username'])
|
|
|
|
# Add user ID to new representation
|
|
|
|
desired_user['id'] = after_user["id"]
|
|
|
|
else:
|
|
|
|
excludes = [
|
|
|
|
"access",
|
|
|
|
"notBefore",
|
|
|
|
"createdTimestamp",
|
|
|
|
"totp",
|
|
|
|
"credentials",
|
|
|
|
"disableableCredentialTypes",
|
|
|
|
"groups",
|
|
|
|
"clientConsents",
|
|
|
|
"federatedIdentities",
|
|
|
|
"requiredActions"]
|
|
|
|
# Add user ID to new representation
|
|
|
|
desired_user['id'] = before_user["id"]
|
|
|
|
|
|
|
|
# Compare users
|
|
|
|
if not (is_struct_included(desired_user, before_user, excludes)): # If the new user does not introduce a change to the existing user
|
|
|
|
# Update the user
|
|
|
|
after_user = kc.update_user(userrep=desired_user, realm=realm)
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
# set user groups
|
|
|
|
if kc.update_user_groups_membership(userrep=desired_user, groups=groups, realm=realm):
|
|
|
|
changed = True
|
|
|
|
# Get the user groups
|
|
|
|
after_user["groups"] = kc.get_user_groups(user_id=desired_user["id"], realm=realm)
|
|
|
|
result["end_state"] = after_user
|
|
|
|
if changed:
|
|
|
|
result["msg"] = 'User %s updated' % (desired_user['username'])
|
|
|
|
else:
|
|
|
|
result["msg"] = 'No changes made for user %s' % (desired_user['username'])
|
|
|
|
|
|
|
|
result['changed'] = changed
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|