diff --git a/plugins/module_utils/identity/keycloak/keycloak.py b/plugins/module_utils/identity/keycloak/keycloak.py index 65f6dc7557..a323441caf 100644 --- a/plugins/module_utils/identity/keycloak/keycloak.py +++ b/plugins/module_utils/identity/keycloak/keycloak.py @@ -103,6 +103,7 @@ URL_REALM_GROUP_ROLEMAPPINGS = "{url}/admin/realms/{realm}/groups/{group}/role-m URL_CLIENTSECRET = "{url}/admin/realms/{realm}/clients/{id}/client-secret" +URL_AUTHENTICATION_AUTHENTICATOR_PROVIDERS = "{url}/admin/realms/{realm}/authentication/authenticator-providers" 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" @@ -2253,6 +2254,19 @@ class KeycloakAPI: except Exception as e: self.fail_request(e, msg=f"Unable to delete role {name} for client {clientid} in realm {realm}: {e}") + def get_authenticator_providers(self, realm: str = "master"): + """ + Get all available authenticator providers of the realm. + :param realm: Realm. + :return: List of authenticator provider representations. + """ + try: + return self._request_and_deserialize( + URL_AUTHENTICATION_AUTHENTICATOR_PROVIDERS.format(url=self.baseurl, realm=realm), method="GET" + ) + except Exception as e: + self.fail_request(e, msg=f"Unable get authenticator providers in realm {realm}: {e}") + def get_authentication_flow_by_alias(self, alias, realm: str = "master"): """ Get an authentication flow by its alias diff --git a/plugins/modules/keycloak_authentication_v2.py b/plugins/modules/keycloak_authentication_v2.py index a51a6000e8..1bdefbfddc 100644 --- a/plugins/modules/keycloak_authentication_v2.py +++ b/plugins/modules/keycloak_authentication_v2.py @@ -810,6 +810,35 @@ def create_authentication_execution_spec_options(depth: int) -> dict[str, t.Any] return options +def validate_executions(kc: KeycloakAPI, realm: str, executions: dict) -> None: + valid_providers = kc.get_authenticator_providers(realm) + valid_provider_ids = {provider["id"] for provider in valid_providers} + + invalid_provider_ids = validate_executions_rec(valid_provider_ids, executions) + if len(invalid_provider_ids) > 0: + invalid_provider_ids_str = ", ".join(f"'{item}'" for item in invalid_provider_ids) + raise ValueError( + f"The following execution providerIds are unknown and therefore invalid: {invalid_provider_ids_str}" + ) + + +def validate_executions_rec(valid_provider_ids: set, executions: dict) -> list: + invalid_provider_ids = [] + for execution in executions: + provider_id = execution["providerId"] + sub_flow = execution["subFlow"] + if provider_id is not None: + if provider_id not in valid_provider_ids: + invalid_provider_ids.append(provider_id) + + if sub_flow is not None: + invalid_provider_ids.extend( + validate_executions_rec(valid_provider_ids, execution["authenticationExecutions"]) + ) + + return invalid_provider_ids + + def main() -> None: """Module entry point.""" argument_spec = keycloak_argument_spec() @@ -869,6 +898,14 @@ def main() -> None: existing_auth_diff_repr = existing_auth_to_diff_repr(kc, realm, existing_auth) try: + try: + validate_executions(kc, realm, desired_auth["authenticationExecutions"]) + except ValueError as e: + module.fail_json( + msg=f"Validation of executions failed: {e}", + exception=traceback.format_exc(), + ) + if not existing_auth: if state == "absent": # The flow does not exist and is not required; nothing to do. diff --git a/tests/integration/targets/keycloak_authentication_v2/tasks/main.yml b/tests/integration/targets/keycloak_authentication_v2/tasks/main.yml index bfadb829e1..c3190b0440 100644 --- a/tests/integration/targets/keycloak_authentication_v2/tasks/main.yml +++ b/tests/integration/targets/keycloak_authentication_v2/tasks/main.yml @@ -31,4 +31,8 @@ - name: Executing flow deletion tests ansible.builtin.include_tasks: - file: tests/test_flow_deletion.yml \ No newline at end of file + file: tests/test_flow_deletion.yml + +- name: Invalid providerIds in execution tests + ansible.builtin.include_tasks: + file: tests/test_invalid_poviderid_flow_creation.yml \ No newline at end of file diff --git a/tests/integration/targets/keycloak_authentication_v2/tasks/tests/test_invalid_poviderid_flow_creation.yml b/tests/integration/targets/keycloak_authentication_v2/tasks/tests/test_invalid_poviderid_flow_creation.yml new file mode 100644 index 0000000000..b0c90ff0ac --- /dev/null +++ b/tests/integration/targets/keycloak_authentication_v2/tasks/tests/test_invalid_poviderid_flow_creation.yml @@ -0,0 +1,62 @@ +# 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: Flow Creation/Update + 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 with invalid providerid in execution + 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: invalid-providerid + requirement: ALTERNATIVE + - subFlow: Integration Test Flow - Handle Existing Account + requirement: ALTERNATIVE + authenticationExecutions: + - providerId: another-invalid-providerid + requirement: REQUIRED + - providerId: auth-cookie + requirement: REQUIRED + ignore_errors: true + register: invalid_providerid_in_flow_result + +- name: Verify that invalid providerId causes failure + ansible.builtin.assert: + that: + - invalid_providerid_in_flow_result is failed + - invalid_providerid_in_flow_result is not changed + - >- + invalid_providerid_in_flow_result.msg == "Validation of executions failed: The following execution providerIds are unknown and therefore invalid: 'invalid-providerid', 'another-invalid-providerid'" + +- name: Retrieve access token + ansible.builtin.include_tasks: + file: ../actions/fetch_access_token.yml + +- name: Assert that the flow did not get created + ansible.builtin.uri: + url: "{{ url }}/admin/realms/{{ realm }}/authentication/flows/Integration%20Test%20Flow%20with%20invalid%20providerid%20in%20execution/executions" + method: GET + headers: + Accept: application/json + User-agent: Ansible + Authorization: "Bearer {{ access_token }}" + return_content: true + status_code: 404 + register: flow_response \ No newline at end of file