Keycloak: add authentication management (#2456) (#2864)

* Allow keycloak_group.py to take token as parameter for the authentification

Refactor get_token to pass module.params + Documentation

Fix unit test and add new one for token as param

Fix identation

Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_clienttemplate.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Allow keycloak_group.py to take token as parameter for the authentification

Refactor get_token to pass module.params + Documentation

* Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Check if base_url is None before to check format

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

Update plugins/modules/identity/keycloak/keycloak_clienttemplate.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

Switch to modern syntax for the documentation (e.g. community.general.keycloak_client)

Update keycloak_client.py

Update keycloak_clienttemplate.py

Add keycloak_authentication module to manage authentication

Minor fixex

Fix indent

* Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Removing variable ANSIBLE_METADATA from beginning of file

Minor fix

Refactoring create_or_update_executions :add change_execution_priority function

Refactoring create_or_update_executions :add create_execution function

Refactoring create_or_update_executions: add create_subflow

Refactoring create_or_update_executions: add update_authentication_executions function

Minor fix

* Using FQCN for the examples

Minor fix

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Update plugins/modules/identity/keycloak/keycloak_authentication.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Refactoring: rename isDictEquals into is_dict_equals

Refactoring: rename variable as authentication_flow

Refactoring: rename variable as new_name

Refactoring: rename variable as flow_list

Refactoring: rename variable as new_flow

Refactoring: changing construction of dict newAuthenticationRepresentation and renaming as new_auth_repr

Minor fix

* Refactoring: rename variables with correct Python syntax (auth_repr, exec_repr)

Move create_or_update_executions function from keycloak.py to keycloak_authentication.py

Minor fix

Remove mock_create_or_update_executions not needed anymore

Fix unit test

Update plugins/module_utils/identity/keycloak/keycloak.py

is_dict_equals function return True if value1 empty

Update plugins/module_utils/identity/keycloak/keycloak.py

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

Rename is_dict_equal as is_struct_included and rename params as struct1 and struct2

Rename variables according to Python naming conventions

Refactoring: add find_exec_in_executions function in keycloak_authentication to remove code duplication

typo

Add blank line

Add required parameter, either creds or token

Typo

try/except only surround for loop containing struct2[key]

Add sub-options to meta_args

assigment of result['changed'] after if-elif-else block

Fix CI error: parameter-type-not-in-doc

Fix unit test: none value excluded from comparison

Minor fix

Simplify is_struct_included function

Replace 'type(..) is' by isinstance(..)

Remove redundant required=True and redundant parenthesis

Add check_mode, check if value is None (None value added by argument spec checker)

Apply suggestions from code review

Update plugins/modules/identity/keycloak/keycloak_authentication.py

* Update plugins/modules/identity/keycloak/keycloak_authentication.py

* Add index paramter to configure the priority order of the execution

* Minor fix: authenticationConfig dict instead of str

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 24c5d4320f)

Co-authored-by: Gaetan2907 <48204380+Gaetan2907@users.noreply.github.com>
This commit is contained in:
patchback[bot] 2021-06-24 19:14:46 +02:00 committed by GitHub
parent 82225e5850
commit 2322937a4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1323 additions and 2 deletions

View file

@ -33,9 +33,9 @@ import json
import traceback
from ansible.module_utils.urls import open_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils._text import to_native
from ansible.module_utils._text import to_native, to_text
URL_REALMS = "{url}/admin/realms"
URL_REALM = "{url}/admin/realms/{realm}"
@ -51,6 +51,17 @@ URL_CLIENTTEMPLATES = "{url}/admin/realms/{realm}/client-templates"
URL_GROUPS = "{url}/admin/realms/{realm}/groups"
URL_GROUP = "{url}/admin/realms/{realm}/groups/{groupid}"
URL_AUTHENTICATION_FLOWS = "{url}/admin/realms/{realm}/authentication/flows"
URL_AUTHENTICATION_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{id}"
URL_AUTHENTICATION_FLOW_COPY = "{url}/admin/realms/{realm}/authentication/flows/{copyfrom}/copy"
URL_AUTHENTICATION_FLOW_EXECUTIONS = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions"
URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/execution"
URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW = "{url}/admin/realms/{realm}/authentication/flows/{flowalias}/executions/flow"
URL_AUTHENTICATION_EXECUTION_CONFIG = "{url}/admin/realms/{realm}/authentication/executions/{id}/config"
URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/raise-priority"
URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY = "{url}/admin/realms/{realm}/authentication/executions/{id}/lower-priority"
URL_AUTHENTICATION_CONFIG = "{url}/admin/realms/{realm}/authentication/config/{id}"
def keycloak_argument_spec():
"""
@ -132,6 +143,59 @@ def get_token(module_params):
}
def is_struct_included(struct1, struct2, exclude=None):
"""
This function compare if the first parameter structure is included in the second.
The function use every elements of struct1 and validates they are present in the struct2 structure.
The two structure does not need to be equals for that function to return true.
Each elements are compared recursively.
:param struct1:
type:
dict for the initial call, can be dict, list, bool, int or str for recursive calls
description:
reference structure
:param struct2:
type:
dict for the initial call, can be dict, list, bool, int or str for recursive calls
description:
structure to compare with first parameter.
:param exclude:
type:
list
description:
Key to exclude from the comparison.
default: None
:return:
type:
bool
description:
Return True if all element of dict 1 are present in dict 2, return false otherwise.
"""
if isinstance(struct1, list) and isinstance(struct2, list):
for item1 in struct1:
if isinstance(item1, (list, dict)):
for item2 in struct2:
if not is_struct_included(item1, item2, exclude):
return False
else:
if item1 not in struct2:
return False
return True
elif isinstance(struct1, dict) and isinstance(struct2, dict):
try:
for key in struct1:
if not (exclude and key in exclude):
if not is_struct_included(struct1[key], struct2[key], exclude):
return False
return True
except KeyError:
return False
elif isinstance(struct1, bool) and isinstance(struct2, bool):
return struct1 == struct2
else:
return to_text(struct1, 'utf-8') == to_text(struct2, 'utf-8')
class KeycloakAPI(object):
""" Keycloak API access; Keycloak uses OAuth 2.0 to protect its API, an access token for which
is obtained through OpenID connect
@ -571,3 +635,254 @@ class KeycloakAPI(object):
except Exception as e:
self.module.fail_json(msg="Unable to delete group %s: %s" % (groupid, str(e)))
def get_authentication_flow_by_alias(self, alias, realm='master'):
"""
Get an authentication flow by it's alias
:param alias: Alias of the authentication flow to get.
:param realm: Realm.
:return: Authentication flow representation.
"""
try:
authentication_flow = {}
# Check if the authentication flow exists on the Keycloak serveraders
authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET', headers=self.restheaders))
for authentication in authentications:
if authentication["alias"] == alias:
authentication_flow = authentication
break
return authentication_flow
except Exception as e:
self.module.fail_json(msg="Unable get authentication flow %s: %s" % (alias, str(e)))
def delete_authentication_flow_by_id(self, id, realm='master'):
"""
Delete an authentication flow from Keycloak
:param id: id of authentication flow to be deleted
:param realm: realm of client to be deleted
:return: HTTPResponse object on success
"""
flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id)
try:
return open_url(flow_url, method='DELETE', headers=self.restheaders,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg='Could not delete authentication flow %s in realm %s: %s'
% (id, realm, str(e)))
def copy_auth_flow(self, config, realm='master'):
"""
Create a new authentication flow from a copy of another.
:param config: Representation of the authentication flow to create.
:param realm: Realm.
:return: Representation of the new authentication flow.
"""
try:
new_name = dict(
newName=config["alias"]
)
open_url(
URL_AUTHENTICATION_FLOW_COPY.format(
url=self.baseurl,
realm=realm,
copyfrom=quote(config["copyFrom"])),
method='POST',
headers=self.restheaders,
data=json.dumps(new_name))
flow_list = json.load(
open_url(
URL_AUTHENTICATION_FLOWS.format(url=self.baseurl,
realm=realm),
method='GET',
headers=self.restheaders))
for flow in flow_list:
if flow["alias"] == config["alias"]:
return flow
return None
except Exception as e:
self.module.fail_json(msg='Could not copy authentication flow %s in realm %s: %s'
% (config["alias"], realm, str(e)))
def create_empty_auth_flow(self, config, realm='master'):
"""
Create a new empty authentication flow.
:param config: Representation of the authentication flow to create.
:param realm: Realm.
:return: Representation of the new authentication flow.
"""
try:
new_flow = dict(
alias=config["alias"],
providerId=config["providerId"],
description=config["description"],
topLevel=True
)
open_url(
URL_AUTHENTICATION_FLOWS.format(
url=self.baseurl,
realm=realm),
method='POST',
headers=self.restheaders,
data=json.dumps(new_flow))
flow_list = json.load(
open_url(
URL_AUTHENTICATION_FLOWS.format(
url=self.baseurl,
realm=realm),
method='GET',
headers=self.restheaders))
for flow in flow_list:
if flow["alias"] == config["alias"]:
return flow
return None
except Exception as e:
self.module.fail_json(msg='Could not create empty authentication flow %s in realm %s: %s'
% (config["alias"], realm, str(e)))
def update_authentication_executions(self, flowAlias, updatedExec, realm='master'):
""" Update authentication executions
:param flowAlias: name of the parent flow
:param updatedExec: JSON containing updated execution
:return: HTTPResponse object on success
"""
try:
open_url(
URL_AUTHENTICATION_FLOW_EXECUTIONS.format(
url=self.baseurl,
realm=realm,
flowalias=quote(flowAlias)),
method='PUT',
headers=self.restheaders,
data=json.dumps(updatedExec))
except Exception as e:
self.module.fail_json(msg="Unable to update executions %s: %s" % (updatedExec, str(e)))
def add_authenticationConfig_to_execution(self, executionId, authenticationConfig, realm='master'):
""" Add autenticatorConfig to the execution
:param executionId: id of execution
:param authenticationConfig: config to add to the execution
:return: HTTPResponse object on success
"""
try:
open_url(
URL_AUTHENTICATION_EXECUTION_CONFIG.format(
url=self.baseurl,
realm=realm,
id=executionId),
method='POST',
headers=self.restheaders,
data=json.dumps(authenticationConfig))
except Exception as e:
self.module.fail_json(msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e)))
def create_subflow(self, subflowName, flowAlias, realm='master'):
""" Create new sublow on the flow
:param subflowName: name of the subflow to create
:param flowAlias: name of the parent flow
:return: HTTPResponse object on success
"""
try:
newSubFlow = {}
newSubFlow["alias"] = subflowName
newSubFlow["provider"] = "registration-page-form"
newSubFlow["type"] = "basic-flow"
open_url(
URL_AUTHENTICATION_FLOW_EXECUTIONS_FLOW.format(
url=self.baseurl,
realm=realm,
flowalias=quote(flowAlias)),
method='POST',
headers=self.restheaders,
data=json.dumps(newSubFlow))
except Exception as e:
self.module.fail_json(msg="Unable to create new subflow %s: %s" % (subflowName, str(e)))
def create_execution(self, execution, flowAlias, realm='master'):
""" Create new execution on the flow
:param execution: name of execution to create
:param flowAlias: name of the parent flow
:return: HTTPResponse object on success
"""
try:
newExec = {}
newExec["provider"] = execution["providerId"]
newExec["requirement"] = execution["requirement"]
open_url(
URL_AUTHENTICATION_FLOW_EXECUTIONS_EXECUTION.format(
url=self.baseurl,
realm=realm,
flowalias=quote(flowAlias)),
method='POST',
headers=self.restheaders,
data=json.dumps(newExec))
except Exception as e:
self.module.fail_json(msg="Unable to create new execution %s: %s" % (execution["provider"], str(e)))
def change_execution_priority(self, executionId, diff, realm='master'):
""" Raise or lower execution priority of diff time
:param executionId: id of execution to lower priority
:param realm: realm the client is in
:param diff: Integer number, raise of diff time if positive lower of diff time if negative
:return: HTTPResponse object on success
"""
try:
if diff > 0:
for i in range(diff):
open_url(
URL_AUTHENTICATION_EXECUTION_RAISE_PRIORITY.format(
url=self.baseurl,
realm=realm,
id=executionId),
method='POST',
headers=self.restheaders)
elif diff < 0:
for i in range(-diff):
open_url(
URL_AUTHENTICATION_EXECUTION_LOWER_PRIORITY.format(
url=self.baseurl,
realm=realm,
id=executionId),
method='POST',
headers=self.restheaders)
except Exception as e:
self.module.fail_json(msg="Unable to change execution priority %s: %s" % (executionId, str(e)))
def get_executions_representation(self, config, realm='master'):
"""
Get a representation of the executions for an authentication flow.
:param config: Representation of the authentication flow
:param realm: Realm
:return: Representation of the executions
"""
try:
# Get executions created
executions = json.load(
open_url(
URL_AUTHENTICATION_FLOW_EXECUTIONS.format(
url=self.baseurl,
realm=realm,
flowalias=quote(config["alias"])),
method='GET',
headers=self.restheaders))
for execution in executions:
if "authenticationConfig" in execution:
execConfigId = execution["authenticationConfig"]
execConfig = json.load(
open_url(
URL_AUTHENTICATION_CONFIG.format(
url=self.baseurl,
realm=realm,
id=execConfigId),
method='GET',
headers=self.restheaders))
execution["authenticationConfig"] = execConfig
return executions
except Exception as e:
self.module.fail_json(msg='Could not get executions for authentication flow %s in realm %s: %s'
% (config["alias"], realm, str(e)))