Skip to content

Commit

Permalink
Inspq keycloak user module (#6476)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
elfelip and felixfontein authored Jun 9, 2023
1 parent 2cfbcb4 commit 07a5f07
Show file tree
Hide file tree
Showing 8 changed files with 1,334 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,8 @@ files:
maintainers: fynncfchen
$modules/keycloak_role.py:
maintainers: laurpaum
$modules/keycloak_user.py:
maintainers: elfelip
$modules/keycloak_user_federation.py:
maintainers: laurpaum
$modules/keycloak_user_rolemapping.py:
Expand Down
251 changes: 251 additions & 0 deletions plugins/module_utils/identity/keycloak/keycloak.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import json
import traceback
import copy

from ansible.module_utils.urls import open_url
from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
Expand Down Expand Up @@ -64,6 +65,14 @@
URL_CLIENT_GROUP_ROLEMAPPINGS_COMPOSITE = "{url}/admin/realms/{realm}/groups/{id}/role-mappings/clients/{client}/composite"

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_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"
Expand Down Expand Up @@ -2382,3 +2391,245 @@ def remove_authz_authorization_scope(self, id, client_id, realm):
validate_certs=self.validate_certs)
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)))

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
Loading

0 comments on commit 07a5f07

Please sign in to comment.