mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-03-21 20:59:10 +00:00
add module keycloak_authentication_v2 (#11557)
* add module keycloak_authentication_v2 * skip sanity checks, because the run into a recursion * 11556 fix documentation * 11556 limit the depth of nested flows to 4 * 11556 code cleanup * 11556 code cleanup - add type hints * 11556 add keycloak_authentication_v2 to meta/runtime.yml * 11556 code cleanup - remove custom type hints * 11556 code cleanup - none checks * Update plugins/modules/keycloak_authentication_v2.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * Update plugins/modules/keycloak_authentication_v2.py Co-authored-by: Felix Fontein <felix@fontein.de> * 11556 code cleanup - remove document starts * 11556 cleanup * 11556 cleanup --------- Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein <felix@fontein.de> Co-authored-by: Thomas Bargetz <thomas.bargetz@rise-world.com>
This commit is contained in:
parent
0e4783dcc3
commit
a69f7e60b4
18 changed files with 1765 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
|
@ -834,6 +834,8 @@ files:
|
|||
maintainers: $team_keycloak
|
||||
$modules/keycloak_authentication.py:
|
||||
maintainers: elfelip Gaetan2907
|
||||
$modules/keycloak_authentication_v2.py:
|
||||
maintainers: thomasbargetz
|
||||
$modules/keycloak_authentication_required_actions.py:
|
||||
maintainers: Skrekulko
|
||||
$modules/keycloak_authz_authorization_scope.py:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ action_groups:
|
|||
keycloak:
|
||||
- keycloak_authentication
|
||||
- keycloak_authentication_required_actions
|
||||
- keycloak_authentication_v2
|
||||
- keycloak_authz_authorization_scope
|
||||
- keycloak_authz_custom_policy
|
||||
- keycloak_authz_permission
|
||||
|
|
|
|||
|
|
@ -2274,6 +2274,35 @@ class KeycloakAPI:
|
|||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Unable get authentication flow {alias}: {e}")
|
||||
|
||||
def get_authentication_flow_by_id(self, id, realm: str = "master"):
|
||||
"""
|
||||
Get an authentication flow by its id
|
||||
:param id: id of the authentication flow to get.
|
||||
:param realm: Realm.
|
||||
:return: Authentication flow representation.
|
||||
"""
|
||||
flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return json.load(self._request(flow_url, method="GET"))
|
||||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Could not get authentication flow {id} in realm {realm}: {e}")
|
||||
|
||||
def update_authentication_flow(self, id, config, realm: str = "master"):
|
||||
"""
|
||||
Updates an authentication flow
|
||||
:param id: id of the authentication flow to update.
|
||||
:param config: Authentication flow configuration.
|
||||
:param realm: Realm.
|
||||
:return: Authentication flow representation.
|
||||
"""
|
||||
flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return self._request(flow_url, method="PUT", data=json.dumps(config))
|
||||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Could not get authentication flow {id} in realm {realm}: {e}")
|
||||
|
||||
def delete_authentication_flow_by_id(self, id, realm: str = "master"):
|
||||
"""
|
||||
Delete an authentication flow from Keycloak
|
||||
|
|
@ -2379,6 +2408,22 @@ class KeycloakAPI:
|
|||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Unable to add authenticationConfig {executionId}: {e}")
|
||||
|
||||
def update_authentication_config(self, configId, authenticationConfig, realm: str = "master"):
|
||||
"""
|
||||
Updates an authentication config
|
||||
:param configId: id of the authentication config
|
||||
:param authenticationConfig: The authentication config
|
||||
:param realm: realm of authentication config
|
||||
"""
|
||||
try:
|
||||
self._request(
|
||||
URL_AUTHENTICATION_CONFIG.format(url=self.baseurl, realm=realm, id=configId),
|
||||
method="PUT",
|
||||
data=json.dumps(authenticationConfig),
|
||||
)
|
||||
except Exception as e:
|
||||
self.fail_request(e, msg=f"Unable to update the authentication config {configId}: {e}")
|
||||
|
||||
def delete_authentication_config(self, configId, realm: str = "master"):
|
||||
"""Delete authenticator config
|
||||
|
||||
|
|
|
|||
980
plugins/modules/keycloak_authentication_v2.py
Normal file
980
plugins/modules/keycloak_authentication_v2.py
Normal file
|
|
@ -0,0 +1,980 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) Ansible project
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: keycloak_authentication_v2
|
||||
|
||||
short_description: Configure authentication flows in Keycloak in an idempotent and safe manner.
|
||||
version_added: 12.5.0
|
||||
description:
|
||||
- This module allows the creation, deletion, and modification of Keycloak authentication flows using the Keycloak REST API.
|
||||
- Rather than modifying an existing flow in place, the module re-creates the flow using the B(Safe Swap) mechanism described below.
|
||||
- B(Safe Swap mechanism) - When an authentication flow needs to be updated, the module never modifies the existing flow in place.
|
||||
Instead it follows a multi-step swap procedure to ensure the flow is never left in an intermediate or unsafe state during the update.
|
||||
This is especially important when the flow is actively bound to a realm binding or a client override,
|
||||
because a partially-updated flow could inadvertently allow unauthorised access.
|
||||
- The B(Safe Swap mechanism) is as follows. 1. A new flow is created under a temporary name (the original alias plus a configurable suffix,
|
||||
for example C(myflow_tmp_for_swap)).
|
||||
2. All executions and their configurations are added to the new temporary flow. 3. If the existing flow is currently bound to a realm or a client,
|
||||
all bindings are redirected to the new temporary flow. This ensures continuity and avoids any gap in active authentication coverage.
|
||||
4. The old flow is deleted. 5. The temporary flow is renamed to the original alias, restoring the expected name.
|
||||
- B(Handling pre-existing temporary swap flows) - If a temporary swap flow already exists (for example, from a previously interrupted run),
|
||||
the module can optionally delete it before proceeding. This behaviour is controlled by the O(force_temporary_swap_flow_deletion) option.
|
||||
If the option is V(false) and a temporary flow already exists, the module will fail to prevent accidental data loss.
|
||||
- B(Idempotency) - If the existing flow already matches the desired configuration, no changes are made.
|
||||
The module compares a normalised representation of the existing flow against the desired state before deciding whether to trigger the Safe Swap procedure.
|
||||
- A depth of 4 sub-flows is supported.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
|
||||
options:
|
||||
realm:
|
||||
description:
|
||||
- The name of the realm in which the authentication flow resides.
|
||||
required: true
|
||||
type: str
|
||||
alias:
|
||||
description:
|
||||
- The name of the authentication flow.
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- A human-readable description of the flow.
|
||||
type: str
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) for the new flow.
|
||||
choices: [basic-flow, client-flow]
|
||||
type: str
|
||||
default: basic-flow
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- The desired execution configuration for the flow.
|
||||
- Executions at root level.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
requirement:
|
||||
description:
|
||||
- The requirement status of the execution or sub-flow.
|
||||
choices: [REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL]
|
||||
type: str
|
||||
required: true
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) of the execution.
|
||||
type: str
|
||||
authenticationConfig:
|
||||
description:
|
||||
- The configuration for the execution.
|
||||
type: dict
|
||||
suboptions:
|
||||
alias:
|
||||
description: Name of the execution config.
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description: Options for the execution config.
|
||||
required: true
|
||||
type: dict
|
||||
subFlow:
|
||||
description:
|
||||
- The name of the sub-flow.
|
||||
type: str
|
||||
subFlowType:
|
||||
description:
|
||||
- The type of the sub-flow.
|
||||
choices: [basic-flow, form-flow]
|
||||
default: basic-flow
|
||||
type: str
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- The execution configuration for executions within the sub-flow.
|
||||
- Executions at sub level 1.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
requirement:
|
||||
description:
|
||||
- The requirement status of the execution or sub-flow.
|
||||
choices: [REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL]
|
||||
type: str
|
||||
required: true
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) of the execution.
|
||||
type: str
|
||||
authenticationConfig:
|
||||
description:
|
||||
- The configuration for the execution.
|
||||
type: dict
|
||||
suboptions:
|
||||
alias:
|
||||
description: Name of the execution config.
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description: Options for the execution config.
|
||||
required: true
|
||||
type: dict
|
||||
subFlow:
|
||||
description:
|
||||
- The name of the sub-flow.
|
||||
type: str
|
||||
subFlowType:
|
||||
description:
|
||||
- The type of the sub-flow.
|
||||
choices: [basic-flow, form-flow]
|
||||
default: basic-flow
|
||||
type: str
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- The execution configuration for executions within the sub-flow.
|
||||
- Executions at sub level 2.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
requirement:
|
||||
description:
|
||||
- The requirement status of the execution or sub-flow.
|
||||
choices: [REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL]
|
||||
type: str
|
||||
required: true
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) of the execution.
|
||||
type: str
|
||||
authenticationConfig:
|
||||
description:
|
||||
- The configuration for the execution.
|
||||
type: dict
|
||||
suboptions:
|
||||
alias:
|
||||
description: Name of the execution config.
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description: Options for the execution config.
|
||||
required: true
|
||||
type: dict
|
||||
subFlow:
|
||||
description:
|
||||
- The name of the sub-flow.
|
||||
type: str
|
||||
subFlowType:
|
||||
description:
|
||||
- The type of the sub-flow.
|
||||
choices: [basic-flow, form-flow]
|
||||
default: basic-flow
|
||||
type: str
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- The execution configuration for executions within the sub-flow.
|
||||
- Executions at sub level 3.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
requirement:
|
||||
description:
|
||||
- The requirement status of the execution or sub-flow.
|
||||
choices: [REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL]
|
||||
type: str
|
||||
required: true
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) of the execution.
|
||||
type: str
|
||||
authenticationConfig:
|
||||
description:
|
||||
- The configuration for the execution.
|
||||
type: dict
|
||||
suboptions:
|
||||
alias:
|
||||
description: Name of the execution config.
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description: Options for the execution config.
|
||||
required: true
|
||||
type: dict
|
||||
subFlow:
|
||||
description:
|
||||
- The name of the sub-flow.
|
||||
type: str
|
||||
subFlowType:
|
||||
description:
|
||||
- The type of the sub-flow.
|
||||
choices: [basic-flow, form-flow]
|
||||
default: basic-flow
|
||||
type: str
|
||||
authenticationExecutions:
|
||||
description:
|
||||
- The execution configuration for executions within the sub-flow.
|
||||
- Executions at sub level 4 (last sub level).
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
requirement:
|
||||
description:
|
||||
- The requirement status of the execution or sub-flow.
|
||||
choices: [REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL]
|
||||
type: str
|
||||
required: true
|
||||
providerId:
|
||||
description:
|
||||
- The C(providerId) of the execution.
|
||||
type: str
|
||||
required: true
|
||||
authenticationConfig:
|
||||
description:
|
||||
- The configuration for the execution.
|
||||
type: dict
|
||||
suboptions:
|
||||
alias:
|
||||
description: Name of the execution config.
|
||||
type: str
|
||||
required: true
|
||||
config:
|
||||
description: Options for the execution config.
|
||||
required: true
|
||||
type: dict
|
||||
state:
|
||||
description:
|
||||
- Whether the authentication flow should exist or not.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
type: str
|
||||
temporary_swap_flow_suffix:
|
||||
description:
|
||||
- The suffix appended to the alias of the temporary flow created during a Safe Swap update.
|
||||
- The temporary flow exists only for the duration of the swap procedure and is renamed to
|
||||
the original alias once all bindings have been successfully transferred.
|
||||
type: str
|
||||
default: _tmp_for_swap
|
||||
force_temporary_swap_flow_deletion:
|
||||
description:
|
||||
- If C(true), any pre-existing temporary swap flow (identified by the original alias plus
|
||||
O(temporary_swap_flow_suffix)) is deleted before the Safe Swap procedure begins.
|
||||
- Set this to C(false) to cause the module to fail instead of silently removing a
|
||||
pre-existing temporary flow, for example to avoid accidental data loss after an
|
||||
interrupted run.
|
||||
default: true
|
||||
type: bool
|
||||
extends_documentation_fragment:
|
||||
- community.general.keycloak
|
||||
- community.general.keycloak.actiongroup_keycloak
|
||||
- community.general.attributes
|
||||
|
||||
author:
|
||||
- Thomas Bargetz (@thomasbargetz)
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create or modify the 'My Login Flow'.
|
||||
community.general.keycloak_authentication_v2:
|
||||
auth_keycloak_url: http://localhost:8080/auth
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: My Login Flow
|
||||
authenticationExecutions:
|
||||
- providerId: idp-review-profile
|
||||
requirement: REQUIRED
|
||||
authenticationConfig:
|
||||
alias: My Login Flow - review profile config
|
||||
config:
|
||||
update.profile.on.first.login: "missing"
|
||||
- subFlow: My Login Flow - User creation or linking
|
||||
requirement: REQUIRED
|
||||
authenticationExecutions:
|
||||
- providerId: idp-create-user-if-unique
|
||||
requirement: ALTERNATIVE
|
||||
authenticationConfig:
|
||||
alias: My Login Flow - create unique user config
|
||||
config:
|
||||
require.password.update.after.registration: "true"
|
||||
- providerId: auth-cookie
|
||||
requirement: REQUIRED
|
||||
- subFlow: My Login Flow - Handle Existing Account
|
||||
requirement: ALTERNATIVE
|
||||
authenticationExecutions:
|
||||
- providerId: idp-confirm-link
|
||||
requirement: REQUIRED
|
||||
- providerId: auth-cookie
|
||||
requirement: DISABLED
|
||||
state: present
|
||||
|
||||
- name: Remove an authentication flow.
|
||||
community.general.keycloak_authentication_v2:
|
||||
auth_keycloak_url: http://localhost:8080/auth
|
||||
auth_realm: master
|
||||
auth_username: admin
|
||||
auth_password: password
|
||||
realm: master
|
||||
alias: My Login Flow
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
end_state:
|
||||
description: Representation of the authentication flow after module execution.
|
||||
returned: on success
|
||||
type: dict
|
||||
sample:
|
||||
{
|
||||
"alias": "My Login Flow",
|
||||
"builtIn": false,
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"id": "bc228863-5887-4297-b898-4d988f8eaa5c",
|
||||
"providerId": "basic-flow",
|
||||
"topLevel": true,
|
||||
"authenticationExecutions": [
|
||||
{
|
||||
"alias": "review profile config",
|
||||
"authenticationConfig": {
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
},
|
||||
"id": "6f09e4fb-aad4-496a-b873-7fa9779df6d7"
|
||||
},
|
||||
"configurable": true,
|
||||
"displayName": "Review Profile",
|
||||
"id": "8f77dab8-2008-416f-989e-88b09ccf0b4c",
|
||||
"index": 0,
|
||||
"level": 0,
|
||||
"providerId": "idp-review-profile",
|
||||
"requirement": "REQUIRED",
|
||||
"requirementChoices": [
|
||||
"REQUIRED",
|
||||
"ALTERNATIVE",
|
||||
"DISABLED"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
import copy
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import (
|
||||
KeycloakAPI,
|
||||
KeycloakError,
|
||||
get_token,
|
||||
keycloak_argument_spec,
|
||||
)
|
||||
|
||||
|
||||
def rename_auth_flow(kc: KeycloakAPI, realm: str, flow_id: str, new_alias: str) -> None:
|
||||
"""Rename an existing authentication flow to a new alias.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which the flow resides.
|
||||
:param flow_id: the ID of the flow to rename.
|
||||
:param new_alias: the new alias to assign to the flow.
|
||||
"""
|
||||
auth = kc.get_authentication_flow_by_id(flow_id, realm)
|
||||
if auth is not None:
|
||||
updated = copy.deepcopy(auth)
|
||||
updated["alias"] = new_alias
|
||||
# The authenticationExecutions key is not accepted by the update endpoint.
|
||||
updated.pop("authenticationExecutions", None)
|
||||
kc.update_authentication_flow(flow_id, config=updated, realm=realm)
|
||||
|
||||
|
||||
def append_suffix_to_executions(executions: list, suffix: str) -> None:
|
||||
"""Recursively append a suffix to all sub-flow and authentication config aliases.
|
||||
|
||||
:param executions: a list of execution dicts to process.
|
||||
:param suffix: the suffix string to append.
|
||||
"""
|
||||
for execution in executions:
|
||||
if execution.get("authenticationConfig") is not None:
|
||||
execution["authenticationConfig"]["alias"] += suffix
|
||||
if execution.get("subFlow") is not None:
|
||||
execution["subFlow"] += suffix
|
||||
if execution.get("authenticationExecutions") is not None:
|
||||
append_suffix_to_executions(execution["authenticationExecutions"], suffix)
|
||||
|
||||
|
||||
def append_suffix_to_flow_names(desired_auth: dict, suffix: str) -> None:
|
||||
"""Append a suffix to the top-level alias and all nested aliases in a flow definition.
|
||||
|
||||
This is used during the Safe Swap procedure to give the temporary flow a distinct name.
|
||||
|
||||
:param desired_auth: the desired authentication flow dict (mutated in place).
|
||||
:param suffix: the suffix string to append.
|
||||
"""
|
||||
desired_auth["alias"] += suffix
|
||||
append_suffix_to_executions(desired_auth["authenticationExecutions"], suffix)
|
||||
|
||||
|
||||
def remove_suffix_from_flow_names(kc: KeycloakAPI, realm: str, auth: dict, suffix: str) -> None:
|
||||
"""Remove a previously-added suffix from the top-level flow alias, all sub-flow aliases,
|
||||
and all authentication config aliases.
|
||||
|
||||
This is the final step of the Safe Swap procedure, which restores the original alias after
|
||||
the temporary flow has been bound and the old flow deleted.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which the flow resides.
|
||||
:param auth: the authentication flow dict (mutated in place to reflect the renamed alias).
|
||||
:param suffix: the suffix to remove.
|
||||
"""
|
||||
new_alias = auth["alias"].removesuffix(suffix)
|
||||
rename_auth_flow(kc, realm, auth["id"], new_alias)
|
||||
auth["alias"] = new_alias
|
||||
|
||||
executions = kc.get_executions_representation(config=auth, realm=realm)
|
||||
for execution in executions:
|
||||
if execution.get("authenticationFlow"):
|
||||
new_sub_flow_alias = execution["displayName"].removesuffix(suffix)
|
||||
rename_auth_flow(kc, realm, execution["flowId"], new_sub_flow_alias)
|
||||
if execution.get("configurable"):
|
||||
auth_config = execution.get("authenticationConfig")
|
||||
if auth_config is not None:
|
||||
auth_config["alias"] = auth_config["alias"].removesuffix(suffix)
|
||||
kc.update_authentication_config(
|
||||
configId=auth_config["id"],
|
||||
authenticationConfig=auth_config,
|
||||
realm=realm,
|
||||
)
|
||||
|
||||
|
||||
def update_execution_requirement_and_config(
|
||||
kc: KeycloakAPI,
|
||||
realm: str,
|
||||
top_level_auth: dict,
|
||||
execution: dict,
|
||||
parent_flow_alias: str,
|
||||
) -> None:
|
||||
"""Update a newly-created execution to set its requirement and, if present, its configuration.
|
||||
|
||||
Keycloak ignores the requirement value on execution creation and defaults all new executions
|
||||
to DISABLED. A subsequent update is therefore required to apply the correct requirement.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which the flow resides.
|
||||
:param top_level_auth: the top-level authentication flow dict used to look up executions.
|
||||
:param execution: the desired execution dict containing 'requirement' and optionally
|
||||
'authenticationConfig'.
|
||||
:param parent_flow_alias: the alias of the flow or sub-flow that owns this execution.
|
||||
"""
|
||||
# The most recently added execution is always last in the list.
|
||||
created_exec = kc.get_executions_representation(top_level_auth, realm=realm)[-1]
|
||||
exec_update = {
|
||||
"id": created_exec["id"],
|
||||
"providerId": execution["providerId"],
|
||||
"requirement": execution["requirement"],
|
||||
"priority": created_exec["priority"],
|
||||
}
|
||||
kc.update_authentication_executions(
|
||||
flowAlias=parent_flow_alias,
|
||||
updatedExec=exec_update,
|
||||
realm=realm,
|
||||
)
|
||||
|
||||
if execution.get("authenticationConfig") is not None:
|
||||
kc.add_authenticationConfig_to_execution(
|
||||
created_exec["id"],
|
||||
execution["authenticationConfig"],
|
||||
realm=realm,
|
||||
)
|
||||
|
||||
|
||||
def create_executions(
|
||||
kc: KeycloakAPI,
|
||||
realm: str,
|
||||
top_level_auth: dict,
|
||||
executions: list,
|
||||
parent_flow_alias: str,
|
||||
) -> None:
|
||||
"""Recursively create all executions and sub-flows under the given parent flow.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which the flow resides.
|
||||
:param top_level_auth: the top-level authentication flow dict, used when querying the
|
||||
current execution list after each creation.
|
||||
:param executions: a list of desired execution dicts to create.
|
||||
:param parent_flow_alias: the alias of the flow or sub-flow that will own the executions.
|
||||
"""
|
||||
for desired_exec in executions:
|
||||
sub_flow = desired_exec["subFlow"]
|
||||
sub_flow_type = desired_exec["subFlowType"]
|
||||
sub_flow_execs = desired_exec.get("authenticationExecutions")
|
||||
|
||||
# Build the minimal payload accepted by the execution creation endpoint.
|
||||
exec_payload = {
|
||||
"providerId": desired_exec.get("providerId"),
|
||||
"requirement": desired_exec["requirement"],
|
||||
}
|
||||
if desired_exec.get("authenticationConfig") is not None:
|
||||
exec_payload["authenticationConfig"] = desired_exec["authenticationConfig"]
|
||||
|
||||
if sub_flow is not None:
|
||||
kc.create_subflow(sub_flow, parent_flow_alias, realm=realm, flowType=sub_flow_type)
|
||||
update_execution_requirement_and_config(kc, realm, top_level_auth, exec_payload, parent_flow_alias)
|
||||
if sub_flow_execs is not None:
|
||||
create_executions(kc, realm, top_level_auth, sub_flow_execs, sub_flow)
|
||||
else:
|
||||
kc.create_execution(exec_payload, flowAlias=parent_flow_alias, realm=realm)
|
||||
update_execution_requirement_and_config(kc, realm, top_level_auth, exec_payload, parent_flow_alias)
|
||||
|
||||
|
||||
def create_empty_flow(kc: KeycloakAPI, realm: str, auth_flow_config: dict) -> dict:
|
||||
"""Create an empty authentication flow from the given configuration dict.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which to create the flow.
|
||||
:param auth_flow_config: the flow configuration dict (must include at least 'alias').
|
||||
:returns: the newly-created flow dict as returned by the Keycloak API.
|
||||
:raises RuntimeError: if the created flow cannot be retrieved immediately after creation.
|
||||
"""
|
||||
created_auth = kc.create_empty_auth_flow(config=auth_flow_config, realm=realm)
|
||||
if created_auth is None:
|
||||
raise RuntimeError(f"Could not retrieve the authentication flow that was just created: {auth_flow_config}")
|
||||
|
||||
return created_auth
|
||||
|
||||
|
||||
def desired_auth_to_diff_repr(desired_auth: dict) -> dict:
|
||||
"""Convert a desired authentication flow dict into the normalized representation used for
|
||||
diff comparison.
|
||||
|
||||
:param desired_auth: the desired flow dict as provided by the module parameters.
|
||||
:returns: a normalized dict suitable for comparison with 'existing_auth_to_diff_repr'.
|
||||
"""
|
||||
desired_copy = copy.deepcopy(desired_auth)
|
||||
desired_copy["topLevel"] = True
|
||||
desired_copy["authenticationExecutions"] = desired_executions_to_diff_repr(desired_copy["authenticationExecutions"])
|
||||
return desired_copy
|
||||
|
||||
|
||||
def desired_executions_to_diff_repr(desired_executions: list) -> list:
|
||||
return desired_executions_to_diff_repr_rec(executions=desired_executions, level=0)
|
||||
|
||||
|
||||
def desired_executions_to_diff_repr_rec(executions: list, level: int) -> list:
|
||||
"""Recursively flatten and normalize a nested execution list into the same flat structure
|
||||
that the Keycloak API returns, so that the two representations can be compared directly.
|
||||
|
||||
:param executions: a list of desired execution dicts (possibly nested).
|
||||
:param level: the current nesting depth (0 for top-level executions).
|
||||
:returns: a flat list of normalized execution dicts.
|
||||
"""
|
||||
converted: list = []
|
||||
for index, execution in enumerate(executions):
|
||||
converted.append(execution)
|
||||
execution["index"] = index
|
||||
execution["priority"] = index
|
||||
execution["level"] = level
|
||||
|
||||
if execution.get("authenticationConfig") is None:
|
||||
execution.pop("authenticationConfig", None)
|
||||
|
||||
if execution.get("subFlow") is not None:
|
||||
execution.pop("providerId", None)
|
||||
execution["authenticationFlow"] = True
|
||||
if execution.get("authenticationExecutions") is not None:
|
||||
converted += desired_executions_to_diff_repr_rec(execution["authenticationExecutions"], level + 1)
|
||||
|
||||
execution.pop("subFlow", None)
|
||||
execution.pop("subFlowType", None)
|
||||
execution.pop("authenticationExecutions", None)
|
||||
|
||||
return converted
|
||||
|
||||
|
||||
def existing_auth_to_diff_repr(kc: KeycloakAPI, realm: str, existing_auth: dict) -> dict:
|
||||
"""Build a normalized representation of an existing flow that can be compared with the
|
||||
output of 'desired_auth_to_diff_repr'.
|
||||
|
||||
Server-side fields that have no equivalent in the desired state (such as 'id',
|
||||
'builtIn', 'requirementChoices', and 'configurable') are stripped so that the
|
||||
comparison is not skewed by fields the user cannot control.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm in which the flow resides.
|
||||
:param existing_auth: the existing flow dict as returned by the Keycloak API.
|
||||
:returns: a normalized dict.
|
||||
"""
|
||||
existing_copy = copy.deepcopy(existing_auth)
|
||||
existing_copy.pop("id", None)
|
||||
existing_copy.pop("builtIn", None)
|
||||
|
||||
executions = kc.get_executions_representation(config=existing_copy, realm=realm)
|
||||
for execution in executions:
|
||||
execution.pop("id", None)
|
||||
execution.pop("requirementChoices", None)
|
||||
execution.pop("configurable", None)
|
||||
execution.pop("displayName", None)
|
||||
execution.pop("description", None)
|
||||
execution.pop("flowId", None)
|
||||
|
||||
if execution.get("authenticationConfig") is not None:
|
||||
execution["authenticationConfig"].pop("id", None)
|
||||
# The alias is already stored inside the authenticationConfig object; the
|
||||
# top-level alias field on the execution is redundant and is removed.
|
||||
execution.pop("alias", None)
|
||||
|
||||
existing_copy["authenticationExecutions"] = executions
|
||||
# Normalize a missing description to None so that it compares equal to an unset desired value.
|
||||
existing_copy["description"] = existing_copy.get("description") or None
|
||||
return existing_copy
|
||||
|
||||
|
||||
def is_auth_flow_in_use(kc: KeycloakAPI, realm: str, existing_auth: dict) -> bool:
|
||||
"""Determine whether the given flow is currently bound to a realm binding or a client
|
||||
authentication flow override.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm to inspect.
|
||||
:param existing_auth: the existing flow dict (must include 'id' and 'alias').
|
||||
:returns: True if the flow is bound anywhere, False otherwise.
|
||||
"""
|
||||
flow_id = existing_auth["id"]
|
||||
flow_alias = existing_auth["alias"]
|
||||
realm_data = kc.get_realm_by_id(realm)
|
||||
if realm_data is None:
|
||||
raise RuntimeError(f"realm '{realm}' does not exist")
|
||||
|
||||
realm_binding_keys = [
|
||||
"browserFlow",
|
||||
"registrationFlow",
|
||||
"directGrantFlow",
|
||||
"resetCredentialsFlow",
|
||||
"clientAuthenticationFlow",
|
||||
"dockerAuthenticationFlow",
|
||||
"firstBrokerLoginFlow",
|
||||
]
|
||||
for binding_key in realm_binding_keys:
|
||||
if realm_data.get(binding_key) == flow_alias:
|
||||
return True
|
||||
|
||||
for client in kc.get_clients(realm=realm):
|
||||
overrides = client.get("authenticationFlowBindingOverrides", {})
|
||||
if overrides.get("browser") == flow_id:
|
||||
return True
|
||||
if overrides.get("direct_grant") == flow_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def rebind_auth_flow_bindings(
|
||||
kc: KeycloakAPI,
|
||||
realm: str,
|
||||
from_id: str,
|
||||
from_alias: str,
|
||||
to_id: str,
|
||||
to_alias: str,
|
||||
) -> None:
|
||||
"""Re-point all realm bindings and client overrides that reference the source flow to the
|
||||
target flow.
|
||||
|
||||
This is the critical step in the Safe Swap procedure that transfers live bindings from the
|
||||
old flow to the newly-created temporary flow without any gap in coverage.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm to update.
|
||||
:param from_id: the ID of the flow to rebind away from.
|
||||
:param from_alias: the alias of the flow to rebind away from.
|
||||
:param to_id: the ID of the flow to rebind to.
|
||||
:param to_alias: the alias of the flow to rebind to.
|
||||
"""
|
||||
realm_data = kc.get_realm_by_id(realm)
|
||||
if realm_data is None:
|
||||
raise RuntimeError(f"realm '{realm}' does not exist")
|
||||
realm_changed = False
|
||||
|
||||
realm_binding_keys = [
|
||||
"browserFlow",
|
||||
"registrationFlow",
|
||||
"directGrantFlow",
|
||||
"resetCredentialsFlow",
|
||||
"clientAuthenticationFlow",
|
||||
"dockerAuthenticationFlow",
|
||||
"firstBrokerLoginFlow",
|
||||
]
|
||||
for binding_key in realm_binding_keys:
|
||||
if realm_data.get(binding_key) == from_alias:
|
||||
realm_data[binding_key] = to_alias
|
||||
realm_changed = True
|
||||
|
||||
if realm_changed:
|
||||
kc.update_realm(realm_data, realm)
|
||||
|
||||
for client in kc.get_clients(realm=realm):
|
||||
overrides = client.get("authenticationFlowBindingOverrides", {})
|
||||
client_changed = False
|
||||
|
||||
if overrides.get("browser") == from_id:
|
||||
client["authenticationFlowBindingOverrides"]["browser"] = to_id
|
||||
client_changed = True
|
||||
if overrides.get("direct_grant") == from_id:
|
||||
client["authenticationFlowBindingOverrides"]["direct_grant"] = to_id
|
||||
client_changed = True
|
||||
|
||||
if client_changed:
|
||||
kc.update_client(id=client["id"], clientrep=client, realm=realm)
|
||||
|
||||
|
||||
def delete_tmp_swap_flow_if_exists(
|
||||
kc: KeycloakAPI,
|
||||
realm: str,
|
||||
tmp_swap_alias: str,
|
||||
fallback_id: str,
|
||||
fallback_alias: str,
|
||||
) -> None:
|
||||
"""Delete a pre-existing temporary swap flow, rebinding any of its bindings back to the
|
||||
fallback flow first to avoid orphaned bindings.
|
||||
|
||||
:param kc: a KeycloakAPI instance.
|
||||
:param realm: the realm to inspect.
|
||||
:param tmp_swap_alias: the alias of the temporary swap flow to delete.
|
||||
:param fallback_id: the ID of the flow to rebind to before deleting the temporary flow.
|
||||
:param fallback_alias: the alias of the flow to rebind to before deleting the temporary flow.
|
||||
"""
|
||||
existing_tmp = kc.get_authentication_flow_by_alias(tmp_swap_alias, realm)
|
||||
if existing_tmp is not None and len(existing_tmp) > 0:
|
||||
rebind_auth_flow_bindings(
|
||||
kc,
|
||||
realm,
|
||||
from_id=existing_tmp["id"],
|
||||
from_alias=existing_tmp["alias"],
|
||||
to_id=fallback_id,
|
||||
to_alias=fallback_alias,
|
||||
)
|
||||
kc.delete_authentication_flow_by_id(id=existing_tmp["id"], realm=realm)
|
||||
|
||||
|
||||
def create_authentication_execution_spec_options(depth: int) -> dict[str, Any]:
|
||||
options: dict[str, Any] = dict(
|
||||
providerId=dict(type="str", required=depth == 0),
|
||||
requirement=dict(type="str", required=True, choices=["REQUIRED", "ALTERNATIVE", "DISABLED", "CONDITIONAL"]),
|
||||
authenticationConfig=dict(
|
||||
type="dict",
|
||||
options=dict(
|
||||
alias=dict(type="str", required=True),
|
||||
config=dict(type="dict", required=True),
|
||||
),
|
||||
),
|
||||
)
|
||||
if depth > 0:
|
||||
options.update(
|
||||
subFlow=dict(type="str"),
|
||||
subFlowType=dict(type="str", choices=["basic-flow", "form-flow"], default="basic-flow"),
|
||||
authenticationExecutions=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=create_authentication_execution_spec_options(depth - 1),
|
||||
),
|
||||
)
|
||||
return options
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Module entry point."""
|
||||
argument_spec = keycloak_argument_spec()
|
||||
|
||||
meta_args = dict(
|
||||
realm=dict(type="str", required=True),
|
||||
alias=dict(type="str", required=True),
|
||||
providerId=dict(type="str", choices=["basic-flow", "client-flow"], default="basic-flow"),
|
||||
description=dict(type="str"),
|
||||
authenticationExecutions=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=create_authentication_execution_spec_options(4),
|
||||
),
|
||||
state=dict(choices=["absent", "present"], default="present"),
|
||||
force_temporary_swap_flow_deletion=dict(type="bool", default=True),
|
||||
temporary_swap_flow_suffix=dict(type="str", default="_tmp_for_swap"),
|
||||
)
|
||||
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", "auth_client_id", "auth_client_secret"]]
|
||||
),
|
||||
required_together=([["auth_username", "auth_password"]]),
|
||||
required_by={"refresh_token": "auth_realm"},
|
||||
)
|
||||
|
||||
result = dict(changed=False, msg="", end_state={})
|
||||
|
||||
# Obtain an access token and initialize the API client.
|
||||
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_swap_deletion = module.params.get("force_temporary_swap_flow_deletion")
|
||||
tmp_swap_suffix = module.params.get("temporary_swap_flow_suffix")
|
||||
|
||||
desired_auth = {
|
||||
"alias": module.params.get("alias"),
|
||||
"providerId": module.params.get("providerId"),
|
||||
"authenticationExecutions": module.params.get("authenticationExecutions") or [],
|
||||
"description": module.params.get("description") or None,
|
||||
}
|
||||
desired_auth_diff_repr = desired_auth_to_diff_repr(desired_auth)
|
||||
|
||||
existing_auth = kc.get_authentication_flow_by_alias(alias=desired_auth["alias"], realm=realm)
|
||||
existing_auth_diff_repr = None
|
||||
if existing_auth:
|
||||
existing_auth_diff_repr = existing_auth_to_diff_repr(kc, realm, existing_auth)
|
||||
|
||||
try:
|
||||
if not existing_auth:
|
||||
if state == "absent":
|
||||
# The flow does not exist and is not required; nothing to do.
|
||||
result["diff"] = dict(before="", after="")
|
||||
result["changed"] = False
|
||||
result["end_state"] = {}
|
||||
result["msg"] = f"'{desired_auth['alias']}' is already absent"
|
||||
module.exit_json(**result)
|
||||
|
||||
elif state == "present":
|
||||
# The flow does not yet exist; create it.
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
result["diff"] = dict(before="", after=desired_auth_diff_repr)
|
||||
module.exit_json(**result)
|
||||
|
||||
created_auth = create_empty_flow(kc, realm, desired_auth)
|
||||
result["changed"] = True
|
||||
|
||||
create_executions(
|
||||
kc=kc,
|
||||
realm=realm,
|
||||
top_level_auth=created_auth,
|
||||
executions=desired_auth["authenticationExecutions"],
|
||||
parent_flow_alias=desired_auth["alias"],
|
||||
)
|
||||
|
||||
exec_repr = kc.get_executions_representation(config=desired_auth, realm=realm)
|
||||
if exec_repr is not None:
|
||||
created_auth["authenticationExecutions"] = exec_repr
|
||||
|
||||
result["diff"] = dict(before="", after=created_auth)
|
||||
result["end_state"] = created_auth
|
||||
result["msg"] = f"Authentication flow '{created_auth['alias']}' with id: '{created_auth['id']}' created"
|
||||
|
||||
else:
|
||||
is_flow_in_use = is_auth_flow_in_use(kc, realm, existing_auth)
|
||||
|
||||
if state == "present":
|
||||
change_required = existing_auth_diff_repr != desired_auth_diff_repr
|
||||
if change_required:
|
||||
result["diff"] = dict(before=existing_auth_diff_repr, after=desired_auth_diff_repr)
|
||||
|
||||
if module.check_mode:
|
||||
result["changed"] = change_required
|
||||
module.exit_json(**result)
|
||||
|
||||
if not change_required:
|
||||
# The existing flow already matches the desired state; nothing to do.
|
||||
result["end_state"] = existing_auth_diff_repr
|
||||
module.exit_json(**result)
|
||||
|
||||
# The flow needs to be updated. Rather than modifying the existing flow in place,
|
||||
# the Safe Swap procedure is used to guarantee that the flow is never left in an
|
||||
# unsafe intermediate state. See the module documentation for a full description.
|
||||
if is_flow_in_use:
|
||||
tmp_swap_alias = desired_auth["alias"] + tmp_swap_suffix
|
||||
|
||||
if force_swap_deletion:
|
||||
# Remove any leftover temporary flow from a previous interrupted run,
|
||||
# rebinding its bindings back to the current flow first.
|
||||
delete_tmp_swap_flow_if_exists(
|
||||
kc=kc,
|
||||
realm=realm,
|
||||
tmp_swap_alias=tmp_swap_alias,
|
||||
fallback_id=existing_auth["id"],
|
||||
fallback_alias=existing_auth["alias"],
|
||||
)
|
||||
|
||||
# Build the new flow under a temporary name so that both flows coexist
|
||||
# during the swap.
|
||||
append_suffix_to_flow_names(desired_auth, tmp_swap_suffix)
|
||||
else:
|
||||
# The flow is not bound anywhere; it is safe to delete it immediately and
|
||||
# recreate it under the original name.
|
||||
kc.delete_authentication_flow_by_id(existing_auth["id"], realm=realm)
|
||||
|
||||
created_auth = create_empty_flow(kc, realm, desired_auth)
|
||||
result["changed"] = True
|
||||
create_executions(
|
||||
kc=kc,
|
||||
realm=realm,
|
||||
top_level_auth=created_auth,
|
||||
executions=desired_auth["authenticationExecutions"],
|
||||
parent_flow_alias=desired_auth["alias"],
|
||||
)
|
||||
|
||||
if is_flow_in_use:
|
||||
# Transfer all bindings from the old flow to the new temporary flow, then
|
||||
# delete the old flow and strip the temporary suffix from all aliases.
|
||||
rebind_auth_flow_bindings(
|
||||
kc=kc,
|
||||
realm=realm,
|
||||
from_id=existing_auth["id"],
|
||||
from_alias=existing_auth["alias"],
|
||||
to_id=created_auth["id"],
|
||||
to_alias=created_auth["alias"],
|
||||
)
|
||||
kc.delete_authentication_flow_by_id(existing_auth["id"], realm=realm)
|
||||
remove_suffix_from_flow_names(kc, realm, created_auth, tmp_swap_suffix)
|
||||
|
||||
created_auth_diff_repr = existing_auth_to_diff_repr(kc, realm, created_auth)
|
||||
result["diff"] = dict(before=existing_auth_diff_repr, after=created_auth_diff_repr)
|
||||
result["end_state"] = created_auth_diff_repr
|
||||
result["msg"] = f"Authentication flow: {created_auth['alias']} id: {created_auth['id']} updated"
|
||||
|
||||
else:
|
||||
if is_flow_in_use:
|
||||
module.fail_json(
|
||||
msg=f"Flow {existing_auth['alias']} with id {existing_auth['id']} is in use and therefore cannot be deleted in realm {realm}"
|
||||
)
|
||||
|
||||
result["diff"] = dict(before=existing_auth_diff_repr, after="")
|
||||
if module.check_mode:
|
||||
result["changed"] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
kc.delete_authentication_flow_by_id(id=existing_auth["id"], realm=realm)
|
||||
result["changed"] = True
|
||||
result["msg"] = f"Authentication flow: {desired_auth['alias']} id: {existing_auth['id']} is deleted"
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg=f"An unexpected error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<!--
|
||||
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_authentication_v2 module integration test
|
||||
|
||||
Run integration tests:
|
||||
|
||||
ansible-test integration -v keycloak_authentication_v2 --allow-unsupported --docker fedora42 --docker-network host
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# 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
|
||||
|
||||
unsupported
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 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
|
||||
- name: Get access token
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/realms/{{ admin_realm }}/protocol/openid-connect/token"
|
||||
method: POST
|
||||
status_code: 200
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
body_format: form-urlencoded
|
||||
body:
|
||||
grant_type: "password"
|
||||
client_id: "admin-cli"
|
||||
username: "{{ admin_user }}"
|
||||
password: "{{ admin_password }}"
|
||||
register: token_response
|
||||
no_log: true
|
||||
|
||||
- name: Extract access token
|
||||
ansible.builtin.set_fact:
|
||||
access_token: "{{ token_response.json['access_token'] }}"
|
||||
no_log: true
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# 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
|
||||
- name: Flow Creation/Update <Integration Test Flow> Version 1
|
||||
community.general.keycloak_authentication_v2:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
alias: Integration Test Flow
|
||||
state: present
|
||||
authenticationExecutions:
|
||||
- providerId: idp-review-profile
|
||||
requirement: REQUIRED
|
||||
authenticationConfig:
|
||||
alias: Integration Test Flow - review profile config
|
||||
config:
|
||||
update.profile.on.first.login: "missing"
|
||||
- subFlow: Integration Test Flow - User creation or linking
|
||||
requirement: REQUIRED
|
||||
authenticationExecutions:
|
||||
- providerId: idp-create-user-if-unique
|
||||
requirement: ALTERNATIVE
|
||||
authenticationConfig:
|
||||
alias: Integration Test Flow - create unique user config
|
||||
config:
|
||||
require.password.update.after.registration: "false"
|
||||
- subFlow: Integration Test Flow - Handle Existing Account
|
||||
requirement: ALTERNATIVE
|
||||
authenticationExecutions:
|
||||
- providerId: idp-confirm-link
|
||||
requirement: REQUIRED
|
||||
- providerId: auth-cookie
|
||||
requirement: REQUIRED
|
||||
register: flow_v1_result
|
||||
|
||||
- name: Expose flow_v1_result to parent scope
|
||||
ansible.builtin.set_fact:
|
||||
flow_v1_result: "{{ flow_v1_result }}"
|
||||
no_log: true
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# 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
|
||||
|
||||
# - adding a new execution inside the sub flow 'Integration Test Flow - User creation or linking' after the execution 'idp-create-user-if-unique'
|
||||
# - disable auth-cookie at the end of the flow
|
||||
# - change the config require.password.update.after.registration to "true"
|
||||
- name: Flow Creation/Update <Integration Test Flow> Version 2
|
||||
community.general.keycloak_authentication_v2:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
alias: Integration Test Flow
|
||||
state: present
|
||||
authenticationExecutions:
|
||||
- providerId: idp-review-profile
|
||||
requirement: REQUIRED
|
||||
authenticationConfig:
|
||||
alias: Integration Test Flow - review profile config
|
||||
config:
|
||||
update.profile.on.first.login: "missing"
|
||||
- subFlow: Integration Test Flow - User creation or linking
|
||||
requirement: REQUIRED
|
||||
authenticationExecutions:
|
||||
- providerId: idp-create-user-if-unique
|
||||
requirement: ALTERNATIVE
|
||||
authenticationConfig:
|
||||
alias: Integration Test Flow - create unique user config
|
||||
config:
|
||||
require.password.update.after.registration: "true"
|
||||
- providerId: auth-cookie
|
||||
requirement: REQUIRED
|
||||
- subFlow: Integration Test Flow - Handle Existing Account
|
||||
requirement: ALTERNATIVE
|
||||
authenticationExecutions:
|
||||
- providerId: idp-confirm-link
|
||||
requirement: REQUIRED
|
||||
- providerId: auth-cookie
|
||||
requirement: DISABLED
|
||||
register: flow_v2_result
|
||||
|
||||
- name: Expose flow_v2_result to parent scope
|
||||
ansible.builtin.set_fact:
|
||||
flow_v2_result: "{{ flow_v2_result }}"
|
||||
no_log: true
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
# 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
|
||||
- name: Retrieve access token
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/fetch_access_token.yml
|
||||
|
||||
- name: Fetch all authentication flows
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: keycloak_flows_response
|
||||
no_log: true
|
||||
|
||||
- name: Extract flow id for alias 'Integration Test Flow'
|
||||
ansible.builtin.set_fact:
|
||||
test_flow_id: >-
|
||||
{{ keycloak_flows_response.json
|
||||
| selectattr('alias', 'equalto', 'Integration Test Flow')
|
||||
| map(attribute='id')
|
||||
| first }}
|
||||
no_log: true
|
||||
|
||||
- name: Fetch basic flow data for 'Integration Test Flow'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/{{ test_flow_id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_top_level_data
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow attributes
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_top_level_data.json.alias == "Integration Test Flow"
|
||||
- test_flow_top_level_data.json.providerId == "basic-flow"
|
||||
- test_flow_top_level_data.json.topLevel == true
|
||||
- test_flow_top_level_data.json.builtIn == false
|
||||
|
||||
- name: Fetch flow executions for 'Integration Test Flow'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/Integration%20Test%20Flow/executions"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_executions
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow executions
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_executions.json | length == 6
|
||||
- test_flow_executions.json[0].providerId == "idp-review-profile"
|
||||
- test_flow_executions.json[0].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[0].index == 0
|
||||
- test_flow_executions.json[0].priority == 0
|
||||
- test_flow_executions.json[0].level == 0
|
||||
- test_flow_executions.json[0].authenticationConfig is not none
|
||||
- test_flow_executions.json[0]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[1].displayName == "Integration Test Flow - User creation or linking"
|
||||
- test_flow_executions.json[1].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[1].index == 1
|
||||
- test_flow_executions.json[1].priority == 1
|
||||
- test_flow_executions.json[1].level == 0
|
||||
- test_flow_executions.json[1]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[1].authenticationFlow == true
|
||||
- test_flow_executions.json[2].providerId == "idp-create-user-if-unique"
|
||||
- test_flow_executions.json[2].requirement == "ALTERNATIVE"
|
||||
- test_flow_executions.json[2].index == 0
|
||||
- test_flow_executions.json[2].priority == 0
|
||||
- test_flow_executions.json[2].level == 1
|
||||
- test_flow_executions.json[2].authenticationConfig is not none
|
||||
- test_flow_executions.json[2]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[3].displayName == "Integration Test Flow - Handle Existing Account"
|
||||
- test_flow_executions.json[3].requirement == "ALTERNATIVE"
|
||||
- test_flow_executions.json[3].index == 1
|
||||
- test_flow_executions.json[3].priority == 1
|
||||
- test_flow_executions.json[3].level == 1
|
||||
- test_flow_executions.json[3]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[3].authenticationFlow == true
|
||||
- test_flow_executions.json[4].providerId == "idp-confirm-link"
|
||||
- test_flow_executions.json[4].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[4].index == 0
|
||||
- test_flow_executions.json[4].priority == 0
|
||||
- test_flow_executions.json[4].level == 2
|
||||
- test_flow_executions.json[4]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[4]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[5].providerId == "auth-cookie"
|
||||
- test_flow_executions.json[5].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[5].index == 2
|
||||
- test_flow_executions.json[5].priority == 2
|
||||
- test_flow_executions.json[5].level == 0
|
||||
- test_flow_executions.json[5]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[5]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
|
||||
- name: Fetch flow execution config for 'idp-review-profile'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/config/{{ test_flow_executions.json[0].authenticationConfig }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_execution_idp_review_profile_config
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow execution config for 'idp-review-profile'
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_execution_idp_review_profile_config.json.alias == "Integration Test Flow - review profile config"
|
||||
- test_flow_execution_idp_review_profile_config.json.config is not none
|
||||
- test_flow_execution_idp_review_profile_config.json.config['update.profile.on.first.login'] == "missing"
|
||||
|
||||
- name: Fetch flow execution config for 'idp-create-user-if-unique'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/config/{{ test_flow_executions.json[2].authenticationConfig }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_execution_idp_create_user_if_unique
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow execution config for 'idp-create-user-if-unique'
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_execution_idp_create_user_if_unique.json.alias == "Integration Test Flow - create unique user config"
|
||||
- test_flow_execution_idp_create_user_if_unique.json.config is not none
|
||||
- test_flow_execution_idp_create_user_if_unique.json.config['require.password.update.after.registration'] == "false"
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
# 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
|
||||
- name: Retrieve access token
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/fetch_access_token.yml
|
||||
|
||||
- name: Fetch all authentication flows
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: keycloak_flows_response
|
||||
no_log: true
|
||||
|
||||
- name: Extract flow id for alias 'Integration Test Flow'
|
||||
ansible.builtin.set_fact:
|
||||
test_flow_id: >-
|
||||
{{ keycloak_flows_response.json
|
||||
| selectattr('alias', 'equalto', 'Integration Test Flow')
|
||||
| map(attribute='id')
|
||||
| first }}
|
||||
no_log: true
|
||||
|
||||
- name: Fetch basic flow data for 'Integration Test Flow'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/{{ test_flow_id }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_top_level_data
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow attributes
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_top_level_data.json.alias == "Integration Test Flow"
|
||||
- test_flow_top_level_data.json.providerId == "basic-flow"
|
||||
- test_flow_top_level_data.json.topLevel == true
|
||||
- test_flow_top_level_data.json.builtIn == false
|
||||
|
||||
- name: Fetch flow executions for 'Integration Test Flow'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/Integration%20Test%20Flow/executions"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_executions
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow executions
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_executions.json | length == 7
|
||||
- test_flow_executions.json[0].providerId == "idp-review-profile"
|
||||
- test_flow_executions.json[0].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[0].index == 0
|
||||
- test_flow_executions.json[0].priority == 0
|
||||
- test_flow_executions.json[0].level == 0
|
||||
- test_flow_executions.json[0].authenticationConfig is not none
|
||||
- test_flow_executions.json[0]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[1].displayName == "Integration Test Flow - User creation or linking"
|
||||
- test_flow_executions.json[1].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[1].index == 1
|
||||
- test_flow_executions.json[1].priority == 1
|
||||
- test_flow_executions.json[1].level == 0
|
||||
- test_flow_executions.json[1]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[1].authenticationFlow == true
|
||||
- test_flow_executions.json[2].providerId == "idp-create-user-if-unique"
|
||||
- test_flow_executions.json[2].requirement == "ALTERNATIVE"
|
||||
- test_flow_executions.json[2].index == 0
|
||||
- test_flow_executions.json[2].priority == 0
|
||||
- test_flow_executions.json[2].level == 1
|
||||
- test_flow_executions.json[2].authenticationConfig is not none
|
||||
- test_flow_executions.json[2]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[3].providerId == "auth-cookie"
|
||||
- test_flow_executions.json[3].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[3].index == 1
|
||||
- test_flow_executions.json[3].priority == 1
|
||||
- test_flow_executions.json[3].level == 1
|
||||
- test_flow_executions.json[3]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[3]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[4].displayName == "Integration Test Flow - Handle Existing Account"
|
||||
- test_flow_executions.json[4].requirement == "ALTERNATIVE"
|
||||
- test_flow_executions.json[4].index == 2
|
||||
- test_flow_executions.json[4].priority == 2
|
||||
- test_flow_executions.json[4].level == 1
|
||||
- test_flow_executions.json[4]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[4].authenticationFlow == true
|
||||
- test_flow_executions.json[5].providerId == "idp-confirm-link"
|
||||
- test_flow_executions.json[5].requirement == "REQUIRED"
|
||||
- test_flow_executions.json[5].index == 0
|
||||
- test_flow_executions.json[5].priority == 0
|
||||
- test_flow_executions.json[5].level == 2
|
||||
- test_flow_executions.json[5]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[5]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
- test_flow_executions.json[6].providerId == "auth-cookie"
|
||||
- test_flow_executions.json[6].requirement == "DISABLED"
|
||||
- test_flow_executions.json[6].index == 2
|
||||
- test_flow_executions.json[6].priority == 2
|
||||
- test_flow_executions.json[6].level == 0
|
||||
- test_flow_executions.json[6]['authenticationConfig'] is not defined
|
||||
- test_flow_executions.json[6]['authenticationFlow'] is not defined or test_flow_executions.json[0].authenticationFlow == false
|
||||
|
||||
- name: Fetch flow execution config for 'idp-review-profile'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/config/{{ test_flow_executions.json[0].authenticationConfig }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_execution_idp_review_profile_config
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow execution config for 'idp-review-profile'
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_execution_idp_review_profile_config.json.alias == "Integration Test Flow - review profile config"
|
||||
- test_flow_execution_idp_review_profile_config.json.config is not none
|
||||
- test_flow_execution_idp_review_profile_config.json.config['update.profile.on.first.login'] == "missing"
|
||||
|
||||
- name: Fetch flow execution config for 'idp-create-user-if-unique'
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/config/{{ test_flow_executions.json[2].authenticationConfig }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: test_flow_execution_idp_create_user_if_unique
|
||||
no_log: true
|
||||
|
||||
- name: Assert flow execution config for 'idp-create-user-if-unique'
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- test_flow_execution_idp_create_user_if_unique.json.alias == "Integration Test Flow - create unique user config"
|
||||
- test_flow_execution_idp_create_user_if_unique.json.config is not none
|
||||
- test_flow_execution_idp_create_user_if_unique.json.config['require.password.update.after.registration'] == "true"
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# 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
|
||||
- name: Install required packages
|
||||
pip:
|
||||
name:
|
||||
- jmespath
|
||||
- requests
|
||||
register: result
|
||||
until: result is success
|
||||
|
||||
- name: Executing Flow Creation tests
|
||||
ansible.builtin.include_tasks:
|
||||
file: tests/test_flow_creation.yml
|
||||
|
||||
- name: Executing unused Flow Modification tests
|
||||
ansible.builtin.include_tasks:
|
||||
file: tests/test_unused_flow_modification.yml
|
||||
|
||||
- name: Executing used Flow Modification tests
|
||||
ansible.builtin.include_tasks:
|
||||
file: tests/test_used_flow_modification.yml
|
||||
|
||||
- name: Executing Flow Deletion tests
|
||||
ansible.builtin.include_tasks:
|
||||
file: tests/test_flow_deletion.yml
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# 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
|
||||
- name: Setup Test
|
||||
ansible.builtin.include_tasks:
|
||||
file: test_setup.yml
|
||||
|
||||
- name: Create the new flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Assert the flow creation result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v1_result is changed
|
||||
- flow_v1_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_v1_result.changed }}, diff={{ flow_v1_result.get('diff') }}"
|
||||
|
||||
- name: Assert the created flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../assertions/assertion_flow_v1.yml
|
||||
|
||||
# Check idempotency, by trying to create the existing flow again
|
||||
- name: Trying to create the <Integration Test Flow> again, although nothing changed
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Assert that the task didn't re-create the flow, because the flow didn't change
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v1_result is not changed
|
||||
- flow_v1_result.diff is not defined
|
||||
msg: "Expected no changes and no diff but got: changed={{ flow_v1_result.changed }}, diff={{ flow_v1_result.get('diff') }}"
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# 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
|
||||
- name: Setup Test
|
||||
ansible.builtin.include_tasks:
|
||||
file: test_setup.yml
|
||||
|
||||
- name: Create the new flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Delete the flow <Integration Test Flow>
|
||||
community.general.keycloak_authentication_v2:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
alias: Integration Test Flow
|
||||
state: absent
|
||||
register: flow_deletion_result
|
||||
|
||||
- name: Assert that the flow deletion result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_deletion_result is changed
|
||||
- flow_deletion_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_deletion_result.changed }}, diff={{ flow_deletion_result.get('diff') }}"
|
||||
|
||||
- name: Retrieve access token
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/fetch_access_token.yml
|
||||
|
||||
- name: Assert that the flow does not exist anymore
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/Integratoin%20Test%20Flow/executions"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
status_code: 404
|
||||
register: flow_response
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# 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
|
||||
- name: Start container
|
||||
community.docker.docker_container:
|
||||
name: mykeycloak
|
||||
image: "quay.io/keycloak/keycloak:{{ keycloak_version }}"
|
||||
command: start-dev
|
||||
env:
|
||||
KC_HTTP_RELATIVE_PATH: /auth
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: password
|
||||
ports:
|
||||
- "{{ keycloak_port }}:8080"
|
||||
detach: true
|
||||
auto_remove: true
|
||||
memory: 2200M
|
||||
|
||||
- name: Wait for Keycloak
|
||||
uri:
|
||||
url: "{{ url }}/admin/"
|
||||
status_code: 200
|
||||
validate_certs: false
|
||||
register: result
|
||||
until: result.status == 200
|
||||
retries: 10
|
||||
delay: 10
|
||||
|
||||
- name: Delete realm if exists
|
||||
community.general.keycloak_realm:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
state: absent
|
||||
|
||||
- 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
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# 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
|
||||
- name: Setup Test
|
||||
ansible.builtin.include_tasks:
|
||||
file: test_setup.yml
|
||||
|
||||
- name: Create the new flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Modify the flow <Integration Test Flow>'
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v2.yml
|
||||
|
||||
- name: Assert the flow creation modification result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v2_result is changed
|
||||
- flow_v2_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_v2_result.changed }}, diff={{ flow_v2_result.get('diff') }}"
|
||||
|
||||
- name: Assert the modified flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../assertions/assertion_flow_v2.yml
|
||||
|
||||
# Revert the modifications from the previous step
|
||||
- name: Revert the modifications the flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Assert the flow modification result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v1_result is changed
|
||||
- flow_v1_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_v1_result.changed }}, diff={{ flow_v1_result.get('diff') }}"
|
||||
|
||||
- name: Assert the flow <Integration Test Flow> after reverting the modifications
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../assertions/assertion_flow_v1.yml
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# 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
|
||||
- name: Setup Test
|
||||
ansible.builtin.include_tasks:
|
||||
file: test_setup.yml
|
||||
|
||||
- name: Create the new flow <Integration Test Flow>
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Assign the flow <Integration Test Flow> as default browser flow
|
||||
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
|
||||
browser_flow: Integration Test Flow
|
||||
|
||||
- name: Modify the flow <Integration Test Flow> although it is bound as realm browser flow
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v2.yml
|
||||
|
||||
- name: Assert the flow modification result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v2_result is changed
|
||||
- flow_v2_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_v2_result.changed }}, diff={{ flow_v2_result.get('diff') }}"
|
||||
|
||||
- name: Assert the flow <Integration Test Flow> after the modifications
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../assertions/assertion_flow_v2.yml
|
||||
|
||||
- name: Fetch realm data to validate if the browser flow is still set to <Integration Test Flow>
|
||||
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
|
||||
register: realm_result
|
||||
|
||||
- name: Assert realm browser flow binding
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- realm_result.end_state.browserFlow == "Integration Test Flow"
|
||||
|
||||
- name: Create a client with browser override <Integration Test Flow>
|
||||
community.general.keycloak_client:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
client_id: "test_client"
|
||||
state: present
|
||||
authentication_flow_binding_overrides:
|
||||
browser_name: "Integration Test Flow"
|
||||
|
||||
- name: Revert the flow modifications of <Integration Test Flow> although it is bound as realm browser and client override flow
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/flow_v1.yml
|
||||
|
||||
- name: Assert the flow modification result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_v1_result is changed
|
||||
- flow_v1_result.diff is defined
|
||||
msg: "Expected changes and a diff but got: changed={{ flow_v1_result.changed }}, diff={{ flow_v1_result.get('diff') }}"
|
||||
|
||||
- name: Assert the flow <Integration Test Flow> after the revert
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../assertions/assertion_flow_v1.yml
|
||||
|
||||
- name: Fetch client data
|
||||
community.general.keycloak_client:
|
||||
auth_keycloak_url: "{{ url }}"
|
||||
auth_realm: "{{ admin_realm }}"
|
||||
auth_username: "{{ admin_user }}"
|
||||
auth_password: "{{ admin_password }}"
|
||||
realm: "{{ realm }}"
|
||||
client_id: "test_client"
|
||||
state: present
|
||||
register: client_result
|
||||
|
||||
- name: Retrieve access token
|
||||
ansible.builtin.include_tasks:
|
||||
file: ../actions/fetch_access_token.yml
|
||||
|
||||
- name: Fetch authentication flow
|
||||
ansible.builtin.uri:
|
||||
url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/{{ client_result.end_state.authenticationFlowBindingOverrides.browser }}"
|
||||
method: GET
|
||||
headers:
|
||||
Accept: application/json
|
||||
User-agent: Ansible
|
||||
Authorization: "Bearer {{ access_token }}"
|
||||
return_content: true
|
||||
register: flow_response
|
||||
|
||||
- name: Assert client flow override binding
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flow_response.json is defined
|
||||
- flow_response.json.alias == "Integration Test Flow"
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# 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
|
||||
keycloak_version: latest
|
||||
keycloak_port: 8080
|
||||
|
||||
url: "http://localhost:{{ keycloak_port }}/auth"
|
||||
admin_realm: master
|
||||
admin_user: admin
|
||||
admin_password: password
|
||||
realm: myrealm
|
||||
|
||||
keycloak_no_otp_required_pattern_orinale: "X-Forwarded-For: 10\\.[0-9\\.:]+"
|
||||
keycloak_no_otp_required_pattern_modifed: "X-Original-Forwarded-For: 10\\.[0-9\\.:]+"
|
||||
Loading…
Add table
Add a link
Reference in a new issue