mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Inspq keycloak user module (#6476)
* Add Keycloak User Module * keycloak_user refactoring * Add changelog fragment for breaking changes * Fix Copyright for keycloak_user module * Add keycloak_user module to BOTMETA * Remove ANSIBLE_METADATA and override aliases for auth_username argument spec * Update plugins/modules/keycloak_user.py Updated short description Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Fix keycloak_user module description Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Dedent and use FQCN's for examples in keycloak_user module * Fix examples in keycloak_user module documentation * keycloak_user refactoring * Add changelog fragment for breaking changes * Remove ANSIBLE_METADATA and override aliases for auth_username argument spec * Fix merge error on keycloak_user module changelogs fragment * Add integration test for keycloak_user module * Fix yamllint errors in keycloak_user integration tests * Add README.md and fix integration tests for keycloak_user module * Add Copyright and license in README.md integration tests keycloak_user module * Update changelogs/fragments/6476-new-keycloak-user.module.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Fix argument_spec auth_username aliases for keycloak_user module * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Add units tests for keycloak_user module * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Remove default value for keycloak_user enabled module parameter * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * keycloak_user refactoring * Remove ANSIBLE_METADATA and override aliases for auth_username argument spec * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Dedent and use FQCN's for examples in keycloak_user module * Fix examples in keycloak_user module documentation * keycloak_user refactoring * Add changelog fragment for breaking changes * Remove ANSIBLE_METADATA and override aliases for auth_username argument spec * Fix merge error on keycloak_user module changelogs fragment * Update changelogs/fragments/6476-new-keycloak-user.module.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Fix argument_spec auth_username aliases for keycloak_user module * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Remove github Workflow * Remove bugfix from changelog fragment * Fix indentation in examples for keycloak_user module * Fix examples in documentation for keycloak_user module * Remove PR 6476 changelog fragment * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/keycloak_user.py Co-authored-by: Felix Fontein <felix@fontein.de> * Restore ansible-test.yml * Add msg output and RETURN documentation for keycloak_user module * Fix RETURN documentation for keycloak_user module * Fix msg for keycloak_user module --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
2cfbcb4efd
commit
07a5f07eaa
8 changed files with 1334 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -707,6 +707,8 @@ files:
|
||||||
maintainers: fynncfchen
|
maintainers: fynncfchen
|
||||||
$modules/keycloak_role.py:
|
$modules/keycloak_role.py:
|
||||||
maintainers: laurpaum
|
maintainers: laurpaum
|
||||||
|
$modules/keycloak_user.py:
|
||||||
|
maintainers: elfelip
|
||||||
$modules/keycloak_user_federation.py:
|
$modules/keycloak_user_federation.py:
|
||||||
maintainers: laurpaum
|
maintainers: laurpaum
|
||||||
$modules/keycloak_user_rolemapping.py:
|
$modules/keycloak_user_rolemapping.py:
|
||||||
|
|
|
@ -9,6 +9,7 @@ __metaclass__ = type
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
import copy
|
||||||
|
|
||||||
from ansible.module_utils.urls import open_url
|
from ansible.module_utils.urls import open_url
|
||||||
from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
|
from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
|
||||||
|
@ -64,6 +65,14 @@ URL_CLIENT_GROUP_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/groups/{id
|
||||||
URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite"
|
URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite"
|
||||||
|
|
||||||
URL_USERS = "{url}/admin/realms/{realm}/users"
|
URL_USERS = "{url}/admin/realms/{realm}/users"
|
||||||
|
URL_USER = "{url}/admin/realms/{realm}/users/{id}"
|
||||||
|
URL_USER_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings"
|
||||||
|
URL_USER_REALM_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm"
|
||||||
|
URL_USER_CLIENTS_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients"
|
||||||
|
URL_USER_CLIENT_ROLE_MAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client_id}"
|
||||||
|
URL_USER_GROUPS = "{url}/admin/realms/{realm}/users/{id}/groups"
|
||||||
|
URL_USER_GROUP = "{url}/admin/realms/{realm}/users/{id}/groups/{group_id}"
|
||||||
|
|
||||||
URL_CLIENT_SERVICE_ACCOUNT_USER = "{url}/admin/realms/{realm}/clients/{id}/service-account-user"
|
URL_CLIENT_SERVICE_ACCOUNT_USER = "{url}/admin/realms/{realm}/clients/{id}/service-account-user"
|
||||||
URL_CLIENT_USER_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}"
|
URL_CLIENT_USER_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}"
|
||||||
URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/available"
|
URL_CLIENT_USER_ROLEMAPPINGS_AVAILABLE = "{url}/admin/realms/{realm}/users/{id}/role-mappings/clients/{client}/available"
|
||||||
|
@ -2382,3 +2391,245 @@ class KeycloakAPI(object):
|
||||||
validate_certs=self.validate_certs)
|
validate_certs=self.validate_certs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.module.fail_json(msg='Could not delete scope %s for client %s in realm %s: %s' % (id, client_id, realm, str(e)))
|
self.module.fail_json(msg='Could not delete scope %s for client %s in realm %s: %s' % (id, client_id, realm, str(e)))
|
||||||
|
|
||||||
|
def get_user_by_id(self, user_id, realm='master'):
|
||||||
|
"""
|
||||||
|
Get a User by its ID.
|
||||||
|
:param user_id: ID of the user.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: Representation of the user.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_url = URL_USER.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=user_id)
|
||||||
|
userrep = json.load(
|
||||||
|
open_url(
|
||||||
|
user_url,
|
||||||
|
method='GET',
|
||||||
|
headers=self.restheaders))
|
||||||
|
return userrep
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not get user %s in realm %s: %s'
|
||||||
|
% (user_id, realm, str(e)))
|
||||||
|
|
||||||
|
def create_user(self, userrep, realm='master'):
|
||||||
|
"""
|
||||||
|
Create a new User.
|
||||||
|
:param userrep: Representation of the user to create
|
||||||
|
:param realm: Realm
|
||||||
|
:return: Representation of the user created.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if 'attributes' in userrep and isinstance(userrep['attributes'], list):
|
||||||
|
attributes = copy.deepcopy(userrep['attributes'])
|
||||||
|
userrep['attributes'] = self.convert_user_attributes_to_keycloak_dict(attributes=attributes)
|
||||||
|
users_url = URL_USERS.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm)
|
||||||
|
open_url(users_url,
|
||||||
|
method='POST',
|
||||||
|
headers=self.restheaders,
|
||||||
|
data=json.dumps(userrep))
|
||||||
|
created_user = self.get_user_by_username(
|
||||||
|
username=userrep['username'],
|
||||||
|
realm=realm)
|
||||||
|
return created_user
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not create user %s in realm %s: %s'
|
||||||
|
% (userrep['username'], realm, str(e)))
|
||||||
|
|
||||||
|
def convert_user_attributes_to_keycloak_dict(self, attributes):
|
||||||
|
keycloak_user_attributes_dict = {}
|
||||||
|
for attribute in attributes:
|
||||||
|
if ('state' not in attribute or attribute['state'] == 'present') and 'name' in attribute:
|
||||||
|
keycloak_user_attributes_dict[attribute['name']] = attribute['values'] if 'values' in attribute else []
|
||||||
|
return keycloak_user_attributes_dict
|
||||||
|
|
||||||
|
def convert_keycloak_user_attributes_dict_to_module_list(self, attributes):
|
||||||
|
module_attributes_list = []
|
||||||
|
for key in attributes:
|
||||||
|
attr = {}
|
||||||
|
attr['name'] = key
|
||||||
|
attr['values'] = attributes[key]
|
||||||
|
module_attributes_list.append(attr)
|
||||||
|
return module_attributes_list
|
||||||
|
|
||||||
|
def update_user(self, userrep, realm='master'):
|
||||||
|
"""
|
||||||
|
Update a User.
|
||||||
|
:param userrep: Representation of the user to update. This representation must include the ID of the user.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: Representation of the updated user.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if 'attributes' in userrep and isinstance(userrep['attributes'], list):
|
||||||
|
attributes = copy.deepcopy(userrep['attributes'])
|
||||||
|
userrep['attributes'] = self.convert_user_attributes_to_keycloak_dict(attributes=attributes)
|
||||||
|
user_url = URL_USER.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=userrep["id"])
|
||||||
|
open_url(
|
||||||
|
user_url,
|
||||||
|
method='PUT',
|
||||||
|
headers=self.restheaders,
|
||||||
|
data=json.dumps(userrep))
|
||||||
|
updated_user = self.get_user_by_id(
|
||||||
|
user_id=userrep['id'],
|
||||||
|
realm=realm)
|
||||||
|
return updated_user
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not update user %s in realm %s: %s'
|
||||||
|
% (userrep['username'], realm, str(e)))
|
||||||
|
|
||||||
|
def delete_user(self, user_id, realm='master'):
|
||||||
|
"""
|
||||||
|
Delete a User.
|
||||||
|
:param user_id: ID of the user to be deleted
|
||||||
|
:param realm: Realm
|
||||||
|
:return: HTTP response.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_url = URL_USER.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=user_id)
|
||||||
|
return open_url(
|
||||||
|
user_url,
|
||||||
|
method='DELETE',
|
||||||
|
headers=self.restheaders)
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not delete user %s in realm %s: %s'
|
||||||
|
% (user_id, realm, str(e)))
|
||||||
|
|
||||||
|
def get_user_groups(self, user_id, realm='master'):
|
||||||
|
"""
|
||||||
|
Get groups for a user.
|
||||||
|
:param user_id: User ID
|
||||||
|
:param realm: Realm
|
||||||
|
:return: Representation of the client groups.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
groups = []
|
||||||
|
user_groups_url = URL_USER_GROUPS.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=user_id)
|
||||||
|
user_groups = json.load(
|
||||||
|
open_url(
|
||||||
|
user_groups_url,
|
||||||
|
method='GET',
|
||||||
|
headers=self.restheaders))
|
||||||
|
for user_group in user_groups:
|
||||||
|
groups.append(user_group["name"])
|
||||||
|
return groups
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not get groups for user %s in realm %s: %s'
|
||||||
|
% (user_id, realm, str(e)))
|
||||||
|
|
||||||
|
def add_user_in_group(self, user_id, group_id, realm='master'):
|
||||||
|
"""
|
||||||
|
Add a user to a group.
|
||||||
|
:param user_id: User ID
|
||||||
|
:param group_id: Group Id to add the user to.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: HTTP Response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_group_url = URL_USER_GROUP.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=user_id,
|
||||||
|
group_id=group_id)
|
||||||
|
return open_url(
|
||||||
|
user_group_url,
|
||||||
|
method='PUT',
|
||||||
|
headers=self.restheaders)
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not add user %s in group %s in realm %s: %s'
|
||||||
|
% (user_id, group_id, realm, str(e)))
|
||||||
|
|
||||||
|
def remove_user_from_group(self, user_id, group_id, realm='master'):
|
||||||
|
"""
|
||||||
|
Remove a user from a group for a user.
|
||||||
|
:param user_id: User ID
|
||||||
|
:param group_id: Group Id to add the user to.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: HTTP response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_group_url = URL_USER_GROUP.format(
|
||||||
|
url=self.baseurl,
|
||||||
|
realm=realm,
|
||||||
|
id=user_id,
|
||||||
|
group_id=group_id)
|
||||||
|
return open_url(
|
||||||
|
user_group_url,
|
||||||
|
method='DELETE',
|
||||||
|
headers=self.restheaders)
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not remove user %s from group %s in realm %s: %s'
|
||||||
|
% (user_id, group_id, realm, str(e)))
|
||||||
|
|
||||||
|
def update_user_groups_membership(self, userrep, groups, realm='master'):
|
||||||
|
"""
|
||||||
|
Update user's group membership
|
||||||
|
:param userrep: Representation of the user. This representation must include the ID.
|
||||||
|
:param realm: Realm
|
||||||
|
:return: True if group membership has been changed. False Otherwise.
|
||||||
|
"""
|
||||||
|
changed = False
|
||||||
|
try:
|
||||||
|
user_existing_groups = self.get_user_groups(
|
||||||
|
user_id=userrep['id'],
|
||||||
|
realm=realm)
|
||||||
|
groups_to_add_and_remove = self.extract_groups_to_add_to_and_remove_from_user(groups)
|
||||||
|
# If group membership need to be changed
|
||||||
|
if not is_struct_included(groups_to_add_and_remove['add'], user_existing_groups):
|
||||||
|
# Get available goups in the realm
|
||||||
|
realm_groups = self.get_groups(realm=realm)
|
||||||
|
for realm_group in realm_groups:
|
||||||
|
if "name" in realm_group and realm_group["name"] in groups_to_add_and_remove['add']:
|
||||||
|
self.add_user_in_group(
|
||||||
|
user_id=userrep["id"],
|
||||||
|
group_id=realm_group["id"],
|
||||||
|
realm=realm)
|
||||||
|
changed = True
|
||||||
|
elif "name" in realm_group and realm_group['name'] in groups_to_add_and_remove['remove']:
|
||||||
|
self.remove_user_from_group(
|
||||||
|
user_id=userrep['id'],
|
||||||
|
group_id=realm_group['id'],
|
||||||
|
realm=realm)
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
except Exception as e:
|
||||||
|
self.module.fail_json(msg='Could not update group membership for user %s in realm %s: %s'
|
||||||
|
% (userrep['id]'], realm, str(e)))
|
||||||
|
|
||||||
|
def extract_groups_to_add_to_and_remove_from_user(self, groups):
|
||||||
|
groups_extract = {}
|
||||||
|
groups_to_add = []
|
||||||
|
groups_to_remove = []
|
||||||
|
if isinstance(groups, list) and len(groups) > 0:
|
||||||
|
for group in groups:
|
||||||
|
group_name = group['name'] if isinstance(group, dict) and 'name' in group else group
|
||||||
|
if isinstance(group, dict) and ('state' not in group or group['state'] == 'present'):
|
||||||
|
groups_to_add.append(group_name)
|
||||||
|
else:
|
||||||
|
groups_to_remove.append(group_name)
|
||||||
|
groups_extract['add'] = groups_to_add
|
||||||
|
groups_extract['remove'] = groups_to_remove
|
||||||
|
|
||||||
|
return groups_extract
|
||||||
|
|
||||||
|
def convert_user_group_list_of_str_to_list_of_dict(self, groups):
|
||||||
|
list_of_groups = []
|
||||||
|
if isinstance(groups, list) and len(groups) > 0:
|
||||||
|
for group in groups:
|
||||||
|
if isinstance(group, str):
|
||||||
|
group_dict = {}
|
||||||
|
group_dict['name'] = group
|
||||||
|
list_of_groups.append(group_dict)
|
||||||
|
return list_of_groups
|
||||||
|
|
542
plugins/modules/keycloak_user.py
Normal file
542
plugins/modules/keycloak_user.py
Normal file
|
@ -0,0 +1,542 @@
|
||||||
|
#!/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:
|
||||||
|
- If C(true), the users are required to reset their credentials at next login.
|
||||||
|
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:
|
||||||
|
- If C(true), allows to remove user and recreate it.
|
||||||
|
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:
|
||||||
|
description: Return C(true) if the operation changed the user on the keycloak server, C(false) otherwise.
|
||||||
|
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 = {}
|
||||||
|
if force: # If the force option is set to true
|
||||||
|
# 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()
|
21
tests/integration/targets/keycloak_user/README.md
Normal file
21
tests/integration/targets/keycloak_user/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
# Running keycloak_user module integration test
|
||||||
|
|
||||||
|
To run Keycloak user module's integration test, start a keycloak server using Docker or Podman:
|
||||||
|
|
||||||
|
podman|docker run -d --rm --name mykeycloak -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=password quay.io/keycloak/keycloak:latest start-dev --http-relative-path /auth
|
||||||
|
|
||||||
|
Source Ansible env-setup from ansible github repository
|
||||||
|
|
||||||
|
Run integration tests:
|
||||||
|
|
||||||
|
ansible-test integration keycloak_user --python 3.10 --allow-unsupported
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
|
||||||
|
podman|docker stop mykeycloak
|
||||||
|
|
4
tests/integration/targets/keycloak_user/aliases
Normal file
4
tests/integration/targets/keycloak_user/aliases
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Copyright (c) 2023, INSPQ Philippe Gauthier (@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
|
||||||
|
unsupported
|
114
tests/integration/targets/keycloak_user/tasks/main.yml
Normal file
114
tests/integration/targets/keycloak_user/tasks/main.yml
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- 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 realm role
|
||||||
|
community.general.keycloak_role:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
name: "{{ role }}"
|
||||||
|
description: "{{ description_1 }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create client
|
||||||
|
community.general.keycloak_client:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: "{{ client_id }}"
|
||||||
|
service_accounts_enabled: true
|
||||||
|
state: present
|
||||||
|
register: client
|
||||||
|
|
||||||
|
|
||||||
|
- name: Create new client role
|
||||||
|
community.general.keycloak_role:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
client_id: "{{ client_id }}"
|
||||||
|
name: "{{ keycloak_client_role }}"
|
||||||
|
description: "{{ description_1 }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create new groups
|
||||||
|
community.general.keycloak_group:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
state: present
|
||||||
|
with_items: "{{ keycloak_user_groups }}"
|
||||||
|
|
||||||
|
- name: Create user
|
||||||
|
community.general.keycloak_user:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
username: "{{ keycloak_username }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
first_name: Ceciestes
|
||||||
|
last_name: Untestes
|
||||||
|
email: ceciestuntestes@test.com
|
||||||
|
groups: "{{ keycloak_user_groups }}"
|
||||||
|
attributes: "{{ keycloak_user_attributes }}"
|
||||||
|
state: present
|
||||||
|
register: create_result
|
||||||
|
|
||||||
|
- name: debug
|
||||||
|
debug:
|
||||||
|
var: create_result
|
||||||
|
|
||||||
|
- name: Assert user is created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- create_result.changed
|
||||||
|
- create_result.end_state.username == 'test'
|
||||||
|
- create_result.end_state.attributes | length == 3
|
||||||
|
- create_result.end_state.groups | length == 2
|
||||||
|
|
||||||
|
- name: Delete User
|
||||||
|
community.general.keycloak_user:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
username: "{{ keycloak_username }}"
|
||||||
|
realm: "{{ realm }}"
|
||||||
|
first_name: Ceciestes
|
||||||
|
last_name: Untestes
|
||||||
|
email: ceciestuntestes@test.com
|
||||||
|
groups: "{{ keycloak_user_groups }}"
|
||||||
|
attributes: "{{ keycloak_user_attributes }}"
|
||||||
|
state: absent
|
||||||
|
register: delete_result
|
||||||
|
|
||||||
|
- name: debug
|
||||||
|
debug:
|
||||||
|
var: delete_result
|
||||||
|
|
||||||
|
- name: Assert user is deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- delete_result.changed
|
||||||
|
- delete_result.end_state | length == 0
|
46
tests/integration/targets/keycloak_user/vars/main.yml
Normal file
46
tests/integration/targets/keycloak_user/vars/main.yml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
# Copyright (c) 2022, Dušan Marković (@bratwurzt)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
url: http://localhost:8080/auth
|
||||||
|
admin_realm: master
|
||||||
|
admin_user: admin
|
||||||
|
admin_password: password
|
||||||
|
realm: myrealm
|
||||||
|
client_id: myclient
|
||||||
|
role: myrole
|
||||||
|
description_1: desc 1
|
||||||
|
description_2: desc 2
|
||||||
|
|
||||||
|
keycloak_username: test
|
||||||
|
keycloak_service_account_client_id: "{{ client_id }}"
|
||||||
|
keycloak_user_realm_roles:
|
||||||
|
- name: offline_access
|
||||||
|
- name: "{{ role }}"
|
||||||
|
keycloak_client_role: test
|
||||||
|
keycloak_user_client_roles:
|
||||||
|
- client_id: "{{ client_id }}"
|
||||||
|
roles:
|
||||||
|
- name: "{{ keycloak_client_role }}"
|
||||||
|
- client_id: "{{ realm }}-realm"
|
||||||
|
roles:
|
||||||
|
- name: view-users
|
||||||
|
- name: query-users
|
||||||
|
keycloak_user_attributes:
|
||||||
|
- name: attr1
|
||||||
|
values:
|
||||||
|
- value1s
|
||||||
|
state: present
|
||||||
|
- name: attr2
|
||||||
|
values:
|
||||||
|
- value2s
|
||||||
|
state: present
|
||||||
|
- name: attr3
|
||||||
|
values:
|
||||||
|
- value3s
|
||||||
|
state: present
|
||||||
|
keycloak_user_groups:
|
||||||
|
- name: test
|
||||||
|
state: present
|
||||||
|
- name: test2
|
354
tests/unit/plugins/modules/test_keycloak_user.py
Normal file
354
tests/unit/plugins/modules/test_keycloak_user.py
Normal file
|
@ -0,0 +1,354 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (c) 2021, 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
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from ansible_collections.community.general.tests.unit.compat import unittest
|
||||||
|
from ansible_collections.community.general.tests.unit.compat.mock import patch
|
||||||
|
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase, set_module_args
|
||||||
|
|
||||||
|
from ansible_collections.community.general.plugins.modules import keycloak_user
|
||||||
|
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
from ansible.module_utils.six import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def patch_keycloak_api(get_user_by_username=None,
|
||||||
|
create_user=None,
|
||||||
|
update_user_groups_membership=None,
|
||||||
|
get_user_groups=None,
|
||||||
|
delete_user=None,
|
||||||
|
update_user=None):
|
||||||
|
"""Mock context manager for patching the methods in KeycloakAPI that contact the Keycloak server
|
||||||
|
|
||||||
|
Patches the `get_user_by_username` and `create_user` methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj = keycloak_user.KeycloakAPI
|
||||||
|
with patch.object(obj, 'get_user_by_username', side_effect=get_user_by_username) as mock_get_user_by_username:
|
||||||
|
with patch.object(obj, 'create_user', side_effect=create_user) as mock_create_user:
|
||||||
|
with patch.object(obj, 'update_user_groups_membership', side_effect=update_user_groups_membership) as mock_update_user_groups_membership:
|
||||||
|
with patch.object(obj, 'get_user_groups', side_effect=get_user_groups) as mock_get_user_groups:
|
||||||
|
with patch.object(obj, 'delete_user', side_effect=delete_user) as mock_delete_user:
|
||||||
|
with patch.object(obj, 'update_user', side_effect=update_user) as mock_update_user:
|
||||||
|
yield mock_get_user_by_username, mock_create_user, mock_update_user_groups_membership,\
|
||||||
|
mock_get_user_groups, mock_delete_user, mock_update_user
|
||||||
|
|
||||||
|
|
||||||
|
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 TestKeycloakUser(ModuleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestKeycloakUser, self).setUp()
|
||||||
|
self.module = keycloak_user
|
||||||
|
|
||||||
|
def test_add_new_user(self):
|
||||||
|
"""Add a new user"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'https: // auth.example.com / auth',
|
||||||
|
'token': '{{ access_token }}',
|
||||||
|
'state': 'present',
|
||||||
|
'realm': 'master',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': []
|
||||||
|
}
|
||||||
|
return_value_get_user_by_username = [None]
|
||||||
|
return_value_update_user_groups_membership = [False]
|
||||||
|
return_get_user_groups = [[]]
|
||||||
|
return_create_user = [{'id': '123eqwdawer24qwdqw4'}]
|
||||||
|
return_delete_user = None
|
||||||
|
return_update_user = None
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_user_by_username=return_value_get_user_by_username,
|
||||||
|
create_user=return_create_user,
|
||||||
|
update_user_groups_membership=return_value_update_user_groups_membership,
|
||||||
|
get_user_groups=return_get_user_groups,
|
||||||
|
update_user=return_update_user,
|
||||||
|
delete_user=return_delete_user) \
|
||||||
|
as (mock_get_user_by_username,
|
||||||
|
mock_create_user,
|
||||||
|
mock_update_user_groups_membership,
|
||||||
|
mock_get_user_groups,
|
||||||
|
mock_delete_user,
|
||||||
|
mock_update_user):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(mock_get_user_by_username.call_count, 1)
|
||||||
|
self.assertEqual(mock_create_user.call_count, 1)
|
||||||
|
self.assertEqual(mock_update_user_groups_membership.call_count, 1)
|
||||||
|
self.assertEqual(mock_get_user_groups.call_count, 1)
|
||||||
|
self.assertEqual(mock_update_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_delete_user.call_count, 0)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_add_exiting_user_no_change(self):
|
||||||
|
"""Add a new user"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'https: // auth.example.com / auth',
|
||||||
|
'token': '{{ access_token }}',
|
||||||
|
'state': 'present',
|
||||||
|
'realm': 'master',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': []
|
||||||
|
}
|
||||||
|
return_value_get_user_by_username = [
|
||||||
|
{
|
||||||
|
'id': '123eqwdawer24qwdqw4',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': [],
|
||||||
|
'enabled': True,
|
||||||
|
'emailVerified': False,
|
||||||
|
'disableableCredentialTypes': [],
|
||||||
|
'requiredActions': [],
|
||||||
|
'credentials': [],
|
||||||
|
'federatedIdentities': [],
|
||||||
|
'clientConsents': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return_value_update_user_groups_membership = [False]
|
||||||
|
return_get_user_groups = [[]]
|
||||||
|
return_create_user = None
|
||||||
|
return_delete_user = None
|
||||||
|
return_update_user = None
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_user_by_username=return_value_get_user_by_username,
|
||||||
|
create_user=return_create_user,
|
||||||
|
update_user_groups_membership=return_value_update_user_groups_membership,
|
||||||
|
get_user_groups=return_get_user_groups,
|
||||||
|
update_user=return_update_user,
|
||||||
|
delete_user=return_delete_user) \
|
||||||
|
as (mock_get_user_by_username,
|
||||||
|
mock_create_user,
|
||||||
|
mock_update_user_groups_membership,
|
||||||
|
mock_get_user_groups,
|
||||||
|
mock_delete_user,
|
||||||
|
mock_update_user):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(mock_get_user_by_username.call_count, 1)
|
||||||
|
self.assertEqual(mock_create_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_update_user_groups_membership.call_count, 1)
|
||||||
|
self.assertEqual(mock_get_user_groups.call_count, 1)
|
||||||
|
self.assertEqual(mock_update_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_delete_user.call_count, 0)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_update_user_with_group_changes(self):
|
||||||
|
"""Update groups for a user"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'https: // auth.example.com / auth',
|
||||||
|
'token': '{{ access_token }}',
|
||||||
|
'state': 'present',
|
||||||
|
'realm': 'master',
|
||||||
|
'username': 'test',
|
||||||
|
'first_name': 'test',
|
||||||
|
'last_name': 'user',
|
||||||
|
'groups': [{
|
||||||
|
'name': 'group1',
|
||||||
|
'state': 'present'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
return_value_get_user_by_username = [
|
||||||
|
{
|
||||||
|
'id': '123eqwdawer24qwdqw4',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': [],
|
||||||
|
'enabled': True,
|
||||||
|
'emailVerified': False,
|
||||||
|
'disableableCredentialTypes': [],
|
||||||
|
'requiredActions': [],
|
||||||
|
'credentials': [],
|
||||||
|
'federatedIdentities': [],
|
||||||
|
'clientConsents': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return_value_update_user_groups_membership = [True]
|
||||||
|
return_get_user_groups = [['group1']]
|
||||||
|
return_create_user = None
|
||||||
|
return_delete_user = None
|
||||||
|
return_update_user = [
|
||||||
|
{
|
||||||
|
'id': '123eqwdawer24qwdqw4',
|
||||||
|
'username': 'test',
|
||||||
|
'first_name': 'test',
|
||||||
|
'last_name': 'user',
|
||||||
|
'enabled': True,
|
||||||
|
'emailVerified': False,
|
||||||
|
'disableableCredentialTypes': [],
|
||||||
|
'requiredActions': [],
|
||||||
|
'credentials': [],
|
||||||
|
'federatedIdentities': [],
|
||||||
|
'clientConsents': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_user_by_username=return_value_get_user_by_username,
|
||||||
|
create_user=return_create_user,
|
||||||
|
update_user_groups_membership=return_value_update_user_groups_membership,
|
||||||
|
get_user_groups=return_get_user_groups,
|
||||||
|
update_user=return_update_user,
|
||||||
|
delete_user=return_delete_user) \
|
||||||
|
as (mock_get_user_by_username,
|
||||||
|
mock_create_user,
|
||||||
|
mock_update_user_groups_membership,
|
||||||
|
mock_get_user_groups,
|
||||||
|
mock_delete_user,
|
||||||
|
mock_update_user):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(mock_get_user_by_username.call_count, 1)
|
||||||
|
self.assertEqual(mock_create_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_update_user_groups_membership.call_count, 1)
|
||||||
|
self.assertEqual(mock_get_user_groups.call_count, 1)
|
||||||
|
self.assertEqual(mock_update_user.call_count, 1)
|
||||||
|
self.assertEqual(mock_delete_user.call_count, 0)
|
||||||
|
|
||||||
|
# Verify that the module's changed status matches what is expected
|
||||||
|
self.assertIs(exec_info.exception.args[0]['changed'], changed)
|
||||||
|
|
||||||
|
def test_delete_user(self):
|
||||||
|
"""Delete a user"""
|
||||||
|
|
||||||
|
module_args = {
|
||||||
|
'auth_keycloak_url': 'https: // auth.example.com / auth',
|
||||||
|
'token': '{{ access_token }}',
|
||||||
|
'state': 'absent',
|
||||||
|
'realm': 'master',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': []
|
||||||
|
}
|
||||||
|
return_value_get_user_by_username = [
|
||||||
|
{
|
||||||
|
'id': '123eqwdawer24qwdqw4',
|
||||||
|
'username': 'test',
|
||||||
|
'groups': [],
|
||||||
|
'enabled': True,
|
||||||
|
'emailVerified': False,
|
||||||
|
'disableableCredentialTypes': [],
|
||||||
|
'requiredActions': [],
|
||||||
|
'credentials': [],
|
||||||
|
'federatedIdentities': [],
|
||||||
|
'clientConsents': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return_value_update_user_groups_membership = None
|
||||||
|
return_get_user_groups = None
|
||||||
|
return_create_user = None
|
||||||
|
return_delete_user = None
|
||||||
|
return_update_user = None
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
# Run the module
|
||||||
|
|
||||||
|
with mock_good_connection():
|
||||||
|
with patch_keycloak_api(get_user_by_username=return_value_get_user_by_username,
|
||||||
|
create_user=return_create_user,
|
||||||
|
update_user_groups_membership=return_value_update_user_groups_membership,
|
||||||
|
get_user_groups=return_get_user_groups,
|
||||||
|
update_user=return_update_user,
|
||||||
|
delete_user=return_delete_user) \
|
||||||
|
as (mock_get_user_by_username,
|
||||||
|
mock_create_user,
|
||||||
|
mock_update_user_groups_membership,
|
||||||
|
mock_get_user_groups,
|
||||||
|
mock_delete_user,
|
||||||
|
mock_update_user):
|
||||||
|
with self.assertRaises(AnsibleExitJson) as exec_info:
|
||||||
|
self.module.main()
|
||||||
|
|
||||||
|
self.assertEqual(mock_get_user_by_username.call_count, 1)
|
||||||
|
self.assertEqual(mock_create_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_update_user_groups_membership.call_count, 0)
|
||||||
|
self.assertEqual(mock_get_user_groups.call_count, 0)
|
||||||
|
self.assertEqual(mock_update_user.call_count, 0)
|
||||||
|
self.assertEqual(mock_delete_user.call_count, 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