#!/usr/bin/python # Copyright (c) 2017, Eike Frost # Copyright (c) 2021, Christophe Gilles # 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_realm short_description: Allows administration of Keycloak realm using Keycloak API version_added: 3.0.0 description: - This module allows the administration of Keycloak realm using the Keycloak REST API. It requires access to the REST API using OpenID Connect; the user connecting and the realm being used must have the requisite access rights. In a default Keycloak installation, admin-cli and an admin user would work, as would a separate realm definition with the scope tailored to your needs and a user having the expected roles. - The names of module options are snake_cased versions of the camelCase ones found in the Keycloak API and its documentation at U(https://www.keycloak.org/docs-api/8.0/rest-api/index.html). Aliases are provided so camelCased versions can be used as well. - The Keycloak API does not always sanity check inputs, for example you can set SAML-specific settings on an OpenID Connect client for instance and also the other way around. B(Be careful). If you do not specify a setting, usually a sensible default is chosen. attributes: check_mode: support: full diff_mode: support: full action_group: version_added: 10.2.0 options: state: description: - State of the realm. - On V(present), the realm is created (or updated if it exists already). - On V(absent), the realm is removed if it exists. choices: ['present', 'absent'] default: 'present' type: str id: description: - The realm to create. type: str realm: description: - The realm name. type: str access_code_lifespan: description: - The realm access code lifespan. aliases: - accessCodeLifespan type: int access_code_lifespan_login: description: - The realm access code lifespan login. aliases: - accessCodeLifespanLogin type: int access_code_lifespan_user_action: description: - The realm access code lifespan user action. aliases: - accessCodeLifespanUserAction type: int access_token_lifespan: description: - The realm access token lifespan. aliases: - accessTokenLifespan type: int access_token_lifespan_for_implicit_flow: description: - The realm access token lifespan for implicit flow. aliases: - accessTokenLifespanForImplicitFlow type: int account_theme: description: - The realm account theme. aliases: - accountTheme type: str action_token_generated_by_admin_lifespan: description: - The realm action token generated by admin lifespan. aliases: - actionTokenGeneratedByAdminLifespan type: int action_token_generated_by_user_lifespan: description: - The realm action token generated by user lifespan. aliases: - actionTokenGeneratedByUserLifespan type: int admin_events_details_enabled: description: - The realm admin events details enabled. aliases: - adminEventsDetailsEnabled type: bool admin_events_enabled: description: - The realm admin events enabled. aliases: - adminEventsEnabled type: bool admin_permissions_enabled: description: - The realm admin permissions enabled. aliases: - adminPermissionsEnabled type: bool version_added: 12.0.0 admin_theme: description: - The realm admin theme. aliases: - adminTheme type: str attributes: description: - The realm attributes. type: dict browser_flow: description: - The realm browser flow. aliases: - browserFlow type: str browser_security_headers: description: - The realm browser security headers. aliases: - browserSecurityHeaders type: dict brute_force_protected: description: - The realm brute force protected. aliases: - bruteForceProtected type: bool brute_force_strategy: description: - The realm brute force strategy. aliases: - bruteForceStrategy choices: ['LINEAR', 'MULTIPLE'] type: str version_added: 11.2.0 client_authentication_flow: description: - The realm client authentication flow. aliases: - clientAuthenticationFlow type: str client_scope_mappings: description: - The realm client scope mappings. aliases: - clientScopeMappings type: dict default_default_client_scopes: description: - The realm default default client scopes. aliases: - defaultDefaultClientScopes type: list elements: str default_groups: description: - The realm default groups. aliases: - defaultGroups type: list elements: str default_locale: description: - The realm default locale. aliases: - defaultLocale type: str default_optional_client_scopes: description: - The realm default optional client scopes. aliases: - defaultOptionalClientScopes type: list elements: str default_roles: description: - The realm default roles. aliases: - defaultRoles type: list elements: str default_signature_algorithm: description: - The realm default signature algorithm. aliases: - defaultSignatureAlgorithm type: str direct_grant_flow: description: - The realm direct grant flow. aliases: - directGrantFlow type: str display_name: description: - The realm display name. aliases: - displayName type: str display_name_html: description: - The realm display name HTML. aliases: - displayNameHtml type: str docker_authentication_flow: description: - The realm docker authentication flow. aliases: - dockerAuthenticationFlow type: str duplicate_emails_allowed: description: - The realm duplicate emails allowed option. aliases: - duplicateEmailsAllowed type: bool edit_username_allowed: description: - The realm edit username allowed option. aliases: - editUsernameAllowed type: bool email_theme: description: - The realm email theme. aliases: - emailTheme type: str enabled: description: - The realm enabled option. type: bool enabled_event_types: description: - The realm enabled event types. aliases: - enabledEventTypes type: list elements: str events_enabled: description: - Enables or disables login events for this realm. aliases: - eventsEnabled type: bool version_added: 3.6.0 events_expiration: description: - The realm events expiration. aliases: - eventsExpiration type: int events_listeners: description: - The realm events listeners. aliases: - eventsListeners type: list elements: str failure_factor: description: - The realm failure factor. aliases: - failureFactor type: int internationalization_enabled: description: - The realm internationalization enabled option. aliases: - internationalizationEnabled type: bool login_theme: description: - The realm login theme. aliases: - loginTheme type: str login_with_email_allowed: description: - The realm login with email allowed option. aliases: - loginWithEmailAllowed type: bool max_delta_time_seconds: description: - The realm max delta time in seconds. aliases: - maxDeltaTimeSeconds type: int max_failure_wait_seconds: description: - The realm max failure wait in seconds. aliases: - maxFailureWaitSeconds type: int max_temporary_lockouts: description: - The realm max temporary lockouts. aliases: - maxTemporaryLockouts type: int version_added: 11.2.0 minimum_quick_login_wait_seconds: description: - The realm minimum quick login wait in seconds. aliases: - minimumQuickLoginWaitSeconds type: int not_before: description: - The realm not before. aliases: - notBefore type: int offline_session_idle_timeout: description: - The realm offline session idle timeout. aliases: - offlineSessionIdleTimeout type: int offline_session_max_lifespan: description: - The realm offline session max lifespan. aliases: - offlineSessionMaxLifespan type: int offline_session_max_lifespan_enabled: description: - The realm offline session max lifespan enabled option. aliases: - offlineSessionMaxLifespanEnabled type: bool otp_policy_algorithm: description: - The realm otp policy algorithm. aliases: - otpPolicyAlgorithm type: str otp_policy_digits: description: - The realm otp policy digits. aliases: - otpPolicyDigits type: int otp_policy_initial_counter: description: - The realm otp policy initial counter. aliases: - otpPolicyInitialCounter type: int otp_policy_look_ahead_window: description: - The realm otp policy look ahead window. aliases: - otpPolicyLookAheadWindow type: int otp_policy_period: description: - The realm otp policy period. aliases: - otpPolicyPeriod type: int otp_policy_type: description: - The realm otp policy type. aliases: - otpPolicyType type: str otp_supported_applications: description: - The realm otp supported applications. aliases: - otpSupportedApplications type: list elements: str password_policy: description: - The realm password policy. aliases: - passwordPolicy type: str organizations_enabled: description: - Enables support for experimental organization feature. aliases: - organizationsEnabled type: bool version_added: 10.0.0 permanent_lockout: description: - The realm permanent lockout. aliases: - permanentLockout type: bool quick_login_check_milli_seconds: description: - The realm quick login check in milliseconds. aliases: - quickLoginCheckMilliSeconds type: int refresh_token_max_reuse: description: - The realm refresh token max reuse. aliases: - refreshTokenMaxReuse type: int registration_allowed: description: - The realm registration allowed option. aliases: - registrationAllowed type: bool registration_email_as_username: description: - The realm registration email as username option. aliases: - registrationEmailAsUsername type: bool registration_flow: description: - The realm registration flow. aliases: - registrationFlow type: str remember_me: description: - The realm remember me option. aliases: - rememberMe type: bool reset_credentials_flow: description: - The realm reset credentials flow. aliases: - resetCredentialsFlow type: str reset_password_allowed: description: - The realm reset password allowed option. aliases: - resetPasswordAllowed type: bool revoke_refresh_token: description: - The realm revoke refresh token option. aliases: - revokeRefreshToken type: bool smtp_server: description: - The realm smtp server. aliases: - smtpServer type: dict ssl_required: description: - The realm ssl required option. choices: ['all', 'external', 'none'] aliases: - sslRequired type: str sso_session_idle_timeout: description: - The realm sso session idle timeout. aliases: - ssoSessionIdleTimeout type: int sso_session_idle_timeout_remember_me: description: - The realm sso session idle timeout remember me. aliases: - ssoSessionIdleTimeoutRememberMe type: int sso_session_max_lifespan: description: - The realm sso session max lifespan. aliases: - ssoSessionMaxLifespan type: int sso_session_max_lifespan_remember_me: description: - The realm sso session max lifespan remember me. aliases: - ssoSessionMaxLifespanRememberMe type: int supported_locales: description: - The realm supported locales. aliases: - supportedLocales type: list elements: str user_managed_access_allowed: description: - The realm user managed access allowed option. aliases: - userManagedAccessAllowed type: bool verify_email: description: - The realm verify email option. aliases: - verifyEmail type: bool wait_increment_seconds: description: - The realm wait increment in seconds. aliases: - waitIncrementSeconds type: int client_session_idle_timeout: description: - All Clients inherit from this setting, time a session is allowed to be idle before it expires. aliases: - clientSessionIdleTimeout type: int version_added: 11.2.0 client_session_max_lifespan: description: - All Clients inherit from this setting, max time before a session is expired. aliases: - clientSessionMaxLifespan type: int version_added: 11.2.0 client_offline_session_idle_timeout: description: - All Clients inherit from this setting, time an offline session is allowed to be idle before it expires. aliases: - clientOfflineSessionIdleTimeout type: int version_added: 11.2.0 client_offline_session_max_lifespan: description: - All Clients inherit from this setting, max time before an offline session is expired regardless of activity. aliases: - clientOfflineSessionMaxLifespan type: int version_added: 11.2.0 oauth2_device_code_lifespan: description: - Max time before the device code and user code are expired. aliases: - oauth2DeviceCodeLifespan type: int version_added: 11.2.0 oauth2_device_polling_interval: description: - The minimum amount of time in seconds that the client should wait between polling requests to the token endpoint. aliases: - oauth2DevicePollingInterval type: int version_added: 11.2.0 web_authn_policy_rp_entity_name: description: - WebAuthn Relying Party Entity Name. aliases: - webAuthnPolicyRpEntityName type: str version_added: 11.3.0 web_authn_policy_signature_algorithms: description: - List of acceptable WebAuthn signature algorithms. aliases: - webAuthnPolicySignatureAlgorithms type: list version_added: 11.3.0 elements: str web_authn_policy_rp_id: description: - WebAuthn Relying Party ID (domain). Empty string means use request host. aliases: - webAuthnPolicyRpId type: str version_added: 11.3.0 web_authn_policy_attestation_conveyance_preference: description: - Attestation conveyance preference for WebAuthn. aliases: - webAuthnPolicyAttestationConveyancePreference type: str version_added: 11.3.0 web_authn_policy_authenticator_attachment: description: - Authenticator attachment preference for WebAuthn authenticators. aliases: - webAuthnPolicyAuthenticatorAttachment type: str version_added: 11.3.0 web_authn_policy_require_resident_key: description: - Whether resident keys are required for WebAuthn (Yes/No/not specified). aliases: - webAuthnPolicyRequireResidentKey type: str version_added: 11.3.0 web_authn_policy_user_verification_requirement: description: - User verification requirement for WebAuthn. aliases: - webAuthnPolicyUserVerificationRequirement type: str version_added: 11.3.0 web_authn_policy_create_timeout: description: - Timeout for WebAuthn credential creation (ms). aliases: - webAuthnPolicyCreateTimeout type: int version_added: 11.3.0 web_authn_policy_avoid_same_authenticator_register: description: - Avoid registering the same authenticator multiple times. aliases: - webAuthnPolicyAvoidSameAuthenticatorRegister type: bool version_added: 11.3.0 web_authn_policy_acceptable_aaguids: description: - List of acceptable AAGUIDs for WebAuthn authenticators. aliases: - webAuthnPolicyAcceptableAaguids type: list version_added: 11.3.0 elements: str web_authn_policy_extra_origins: description: - Additional acceptable origins for WebAuthn requests. aliases: - webAuthnPolicyExtraOrigins type: list version_added: 11.3.0 elements: str web_authn_policy_passwordless_rp_entity_name: description: - WebAuthn Passwordless Relying Party Entity Name. aliases: - webAuthnPolicyPasswordlessRpEntityName type: str version_added: 11.3.0 web_authn_policy_passwordless_signature_algorithms: description: - List of acceptable WebAuthn signature algorithms for passwordless. aliases: - webAuthnPolicyPasswordlessSignatureAlgorithms type: list version_added: 11.3.0 elements: str web_authn_policy_passwordless_rp_id: description: - WebAuthn Passwordless Relying Party ID (domain). aliases: - webAuthnPolicyPasswordlessRpId type: str version_added: 11.3.0 web_authn_policy_passwordless_attestation_conveyance_preference: description: - Attestation conveyance preference for WebAuthn passwordless. aliases: - webAuthnPolicyPasswordlessAttestationConveyancePreference type: str version_added: 11.3.0 web_authn_policy_passwordless_authenticator_attachment: description: - Authenticator attachment for WebAuthn passwordless. aliases: - webAuthnPolicyPasswordlessAuthenticatorAttachment type: str version_added: 11.3.0 web_authn_policy_passwordless_require_resident_key: description: - Whether resident keys are required for WebAuthn passwordless (V(Yes)/V(No)/V(not specified)). aliases: - webAuthnPolicyPasswordlessRequireResidentKey type: str version_added: 11.3.0 web_authn_policy_passwordless_user_verification_requirement: description: - User verification requirement for WebAuthn passwordless. aliases: - webAuthnPolicyPasswordlessUserVerificationRequirement type: str version_added: 11.3.0 web_authn_policy_passwordless_create_timeout: description: - Timeout for WebAuthn passwordless credential creation (ms). aliases: - webAuthnPolicyPasswordlessCreateTimeout type: int version_added: 11.3.0 web_authn_policy_passwordless_avoid_same_authenticator_register: description: - Avoid registering the same authenticator multiple times for passwordless. aliases: - webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister type: bool version_added: 11.3.0 web_authn_policy_passwordless_acceptable_aaguids: description: - List of acceptable AAGUIDs for WebAuthn passwordless authenticators. aliases: - webAuthnPolicyPasswordlessAcceptableAaguids type: list version_added: 11.3.0 elements: str web_authn_policy_passwordless_extra_origins: description: - Additional acceptable origins for WebAuthn passwordless requests. aliases: - webAuthnPolicyPasswordlessExtraOrigins type: list version_added: 11.3.0 elements: str web_authn_policy_passwordless_passkeys_enabled: description: - Enable passkeys (conditional UI) authentication in the username forms. aliases: - webAuthnPolicyPasswordlessPasskeysEnabled type: bool version_added: 12.1.0 extends_documentation_fragment: - community.general.keycloak - community.general.keycloak.actiongroup_keycloak - community.general.attributes author: - Christophe Gilles (@kris2kris) """ EXAMPLES = r""" - name: Create or update Keycloak realm (minimal example) community.general.keycloak_realm: auth_client_id: admin-cli auth_keycloak_url: https://auth.example.com/auth auth_realm: master auth_username: USERNAME auth_password: PASSWORD realm: unique_realm_name state: present - name: Delete a Keycloak realm community.general.keycloak_realm: auth_client_id: admin-cli auth_keycloak_url: https://auth.example.com/auth auth_realm: master auth_username: USERNAME auth_password: PASSWORD realm: unique_realm_name state: absent """ RETURN = r""" msg: description: Message as to what action was taken. returned: always type: str sample: "Realm testrealm has been updated" proposed: description: Representation of proposed realm. returned: always type: dict sample: {"realm": "test"} existing: description: Representation of existing realm (sample is truncated). returned: always type: dict sample: { "adminUrl": "http://www.example.com/admin_url", "attributes": { "request.object.signature.alg": "RS256" } } end_state: description: Representation of realm after module execution (sample is truncated). returned: on success type: dict sample: { "adminUrl": "http://www.example.com/admin_url", "attributes": { "request.object.signature.alg": "RS256" } } """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import ( KeycloakAPI, KeycloakError, camel, get_token, keycloak_argument_spec, ) def normalise_cr(realmrep): """Re-sorts any properties where the order is important so that diff's is minimised and the change detection is more effective. :param realmrep: the realmrep dict to be sanitized :return: normalised realmrep dict """ # Avoid the dict passed in to be modified realmrep = realmrep.copy() if "enabledEventTypes" in realmrep: realmrep["enabledEventTypes"] = list(sorted(realmrep["enabledEventTypes"])) if "otpSupportedApplications" in realmrep: realmrep["otpSupportedApplications"] = list(sorted(realmrep["otpSupportedApplications"])) if "supportedLocales" in realmrep: realmrep["supportedLocales"] = list(sorted(realmrep["supportedLocales"])) return realmrep def sanitize_cr(realmrep): """Removes probably sensitive details from a realm representation. :param realmrep: the realmrep dict to be sanitized :return: sanitized realmrep dict """ if not realmrep: return realmrep result = realmrep.copy() if "secret" in result: result["secret"] = "********" if "attributes" in result: if "saml.signing.private.key" in result["attributes"]: result["attributes"] = result["attributes"].copy() result["attributes"]["saml.signing.private.key"] = "********" return normalise_cr(result) def main(): """ Module execution :return: """ argument_spec = keycloak_argument_spec() meta_args = dict( state=dict(default="present", choices=["present", "absent"]), id=dict(type="str"), realm=dict(type="str"), access_code_lifespan=dict(type="int", aliases=["accessCodeLifespan"]), access_code_lifespan_login=dict(type="int", aliases=["accessCodeLifespanLogin"]), access_code_lifespan_user_action=dict(type="int", aliases=["accessCodeLifespanUserAction"]), access_token_lifespan=dict(type="int", aliases=["accessTokenLifespan"], no_log=False), access_token_lifespan_for_implicit_flow=dict( type="int", aliases=["accessTokenLifespanForImplicitFlow"], no_log=False ), account_theme=dict(type="str", aliases=["accountTheme"]), action_token_generated_by_admin_lifespan=dict( type="int", aliases=["actionTokenGeneratedByAdminLifespan"], no_log=False ), action_token_generated_by_user_lifespan=dict( type="int", aliases=["actionTokenGeneratedByUserLifespan"], no_log=False ), admin_events_details_enabled=dict(type="bool", aliases=["adminEventsDetailsEnabled"]), admin_events_enabled=dict(type="bool", aliases=["adminEventsEnabled"]), admin_permissions_enabled=dict(type="bool", aliases=["adminPermissionsEnabled"]), admin_theme=dict(type="str", aliases=["adminTheme"]), attributes=dict(type="dict"), browser_flow=dict(type="str", aliases=["browserFlow"]), browser_security_headers=dict(type="dict", aliases=["browserSecurityHeaders"]), brute_force_protected=dict(type="bool", aliases=["bruteForceProtected"]), brute_force_strategy=dict(type="str", choices=["LINEAR", "MULTIPLE"], aliases=["bruteForceStrategy"]), client_authentication_flow=dict(type="str", aliases=["clientAuthenticationFlow"]), client_scope_mappings=dict(type="dict", aliases=["clientScopeMappings"]), default_default_client_scopes=dict(type="list", elements="str", aliases=["defaultDefaultClientScopes"]), default_groups=dict(type="list", elements="str", aliases=["defaultGroups"]), default_locale=dict(type="str", aliases=["defaultLocale"]), default_optional_client_scopes=dict(type="list", elements="str", aliases=["defaultOptionalClientScopes"]), default_roles=dict(type="list", elements="str", aliases=["defaultRoles"]), default_signature_algorithm=dict(type="str", aliases=["defaultSignatureAlgorithm"]), direct_grant_flow=dict(type="str", aliases=["directGrantFlow"]), display_name=dict(type="str", aliases=["displayName"]), display_name_html=dict(type="str", aliases=["displayNameHtml"]), docker_authentication_flow=dict(type="str", aliases=["dockerAuthenticationFlow"]), duplicate_emails_allowed=dict(type="bool", aliases=["duplicateEmailsAllowed"]), edit_username_allowed=dict(type="bool", aliases=["editUsernameAllowed"]), email_theme=dict(type="str", aliases=["emailTheme"]), enabled=dict(type="bool"), enabled_event_types=dict(type="list", elements="str", aliases=["enabledEventTypes"]), events_enabled=dict(type="bool", aliases=["eventsEnabled"]), events_expiration=dict(type="int", aliases=["eventsExpiration"]), events_listeners=dict(type="list", elements="str", aliases=["eventsListeners"]), failure_factor=dict(type="int", aliases=["failureFactor"]), internationalization_enabled=dict(type="bool", aliases=["internationalizationEnabled"]), login_theme=dict(type="str", aliases=["loginTheme"]), login_with_email_allowed=dict(type="bool", aliases=["loginWithEmailAllowed"]), max_delta_time_seconds=dict(type="int", aliases=["maxDeltaTimeSeconds"]), max_failure_wait_seconds=dict(type="int", aliases=["maxFailureWaitSeconds"]), max_temporary_lockouts=dict(type="int", aliases=["maxTemporaryLockouts"]), minimum_quick_login_wait_seconds=dict(type="int", aliases=["minimumQuickLoginWaitSeconds"]), not_before=dict(type="int", aliases=["notBefore"]), offline_session_idle_timeout=dict(type="int", aliases=["offlineSessionIdleTimeout"]), offline_session_max_lifespan=dict(type="int", aliases=["offlineSessionMaxLifespan"]), offline_session_max_lifespan_enabled=dict(type="bool", aliases=["offlineSessionMaxLifespanEnabled"]), otp_policy_algorithm=dict(type="str", aliases=["otpPolicyAlgorithm"]), otp_policy_digits=dict(type="int", aliases=["otpPolicyDigits"]), otp_policy_initial_counter=dict(type="int", aliases=["otpPolicyInitialCounter"]), otp_policy_look_ahead_window=dict(type="int", aliases=["otpPolicyLookAheadWindow"]), otp_policy_period=dict(type="int", aliases=["otpPolicyPeriod"]), otp_policy_type=dict(type="str", aliases=["otpPolicyType"]), otp_supported_applications=dict(type="list", elements="str", aliases=["otpSupportedApplications"]), password_policy=dict(type="str", aliases=["passwordPolicy"], no_log=False), organizations_enabled=dict(type="bool", aliases=["organizationsEnabled"]), permanent_lockout=dict(type="bool", aliases=["permanentLockout"]), quick_login_check_milli_seconds=dict(type="int", aliases=["quickLoginCheckMilliSeconds"]), refresh_token_max_reuse=dict(type="int", aliases=["refreshTokenMaxReuse"], no_log=False), registration_allowed=dict(type="bool", aliases=["registrationAllowed"]), registration_email_as_username=dict(type="bool", aliases=["registrationEmailAsUsername"]), registration_flow=dict(type="str", aliases=["registrationFlow"]), remember_me=dict(type="bool", aliases=["rememberMe"]), reset_credentials_flow=dict(type="str", aliases=["resetCredentialsFlow"]), reset_password_allowed=dict(type="bool", aliases=["resetPasswordAllowed"], no_log=False), revoke_refresh_token=dict(type="bool", aliases=["revokeRefreshToken"]), smtp_server=dict(type="dict", aliases=["smtpServer"]), ssl_required=dict(choices=["external", "all", "none"], aliases=["sslRequired"]), sso_session_idle_timeout=dict(type="int", aliases=["ssoSessionIdleTimeout"]), sso_session_idle_timeout_remember_me=dict(type="int", aliases=["ssoSessionIdleTimeoutRememberMe"]), sso_session_max_lifespan=dict(type="int", aliases=["ssoSessionMaxLifespan"]), sso_session_max_lifespan_remember_me=dict(type="int", aliases=["ssoSessionMaxLifespanRememberMe"]), supported_locales=dict(type="list", elements="str", aliases=["supportedLocales"]), user_managed_access_allowed=dict(type="bool", aliases=["userManagedAccessAllowed"]), verify_email=dict(type="bool", aliases=["verifyEmail"]), wait_increment_seconds=dict(type="int", aliases=["waitIncrementSeconds"]), client_session_idle_timeout=dict(type="int", aliases=["clientSessionIdleTimeout"]), client_session_max_lifespan=dict(type="int", aliases=["clientSessionMaxLifespan"]), client_offline_session_idle_timeout=dict(type="int", aliases=["clientOfflineSessionIdleTimeout"]), client_offline_session_max_lifespan=dict(type="int", aliases=["clientOfflineSessionMaxLifespan"]), oauth2_device_code_lifespan=dict(type="int", aliases=["oauth2DeviceCodeLifespan"]), oauth2_device_polling_interval=dict(type="int", aliases=["oauth2DevicePollingInterval"]), web_authn_policy_rp_entity_name=dict(type="str", aliases=["webAuthnPolicyRpEntityName"]), web_authn_policy_signature_algorithms=dict( type="list", elements="str", aliases=["webAuthnPolicySignatureAlgorithms"] ), web_authn_policy_rp_id=dict(type="str", aliases=["webAuthnPolicyRpId"]), web_authn_policy_attestation_conveyance_preference=dict( type="str", aliases=["webAuthnPolicyAttestationConveyancePreference"] ), web_authn_policy_authenticator_attachment=dict(type="str", aliases=["webAuthnPolicyAuthenticatorAttachment"]), web_authn_policy_require_resident_key=dict( type="str", aliases=["webAuthnPolicyRequireResidentKey"], no_log=False ), web_authn_policy_user_verification_requirement=dict( type="str", aliases=["webAuthnPolicyUserVerificationRequirement"] ), web_authn_policy_create_timeout=dict(type="int", aliases=["webAuthnPolicyCreateTimeout"]), web_authn_policy_avoid_same_authenticator_register=dict( type="bool", aliases=["webAuthnPolicyAvoidSameAuthenticatorRegister"] ), web_authn_policy_acceptable_aaguids=dict( type="list", elements="str", aliases=["webAuthnPolicyAcceptableAaguids"] ), web_authn_policy_extra_origins=dict(type="list", elements="str", aliases=["webAuthnPolicyExtraOrigins"]), web_authn_policy_passwordless_rp_entity_name=dict( type="str", aliases=["webAuthnPolicyPasswordlessRpEntityName"] ), web_authn_policy_passwordless_signature_algorithms=dict( type="list", elements="str", aliases=["webAuthnPolicyPasswordlessSignatureAlgorithms"], no_log=False ), web_authn_policy_passwordless_rp_id=dict(type="str", aliases=["webAuthnPolicyPasswordlessRpId"]), web_authn_policy_passwordless_attestation_conveyance_preference=dict( type="str", aliases=["webAuthnPolicyPasswordlessAttestationConveyancePreference"], no_log=False ), web_authn_policy_passwordless_authenticator_attachment=dict( type="str", aliases=["webAuthnPolicyPasswordlessAuthenticatorAttachment"], no_log=False ), web_authn_policy_passwordless_require_resident_key=dict( type="str", aliases=["webAuthnPolicyPasswordlessRequireResidentKey"], no_log=False ), web_authn_policy_passwordless_user_verification_requirement=dict( type="str", aliases=["webAuthnPolicyPasswordlessUserVerificationRequirement"], no_log=False ), web_authn_policy_passwordless_create_timeout=dict( type="int", aliases=["webAuthnPolicyPasswordlessCreateTimeout"] ), web_authn_policy_passwordless_avoid_same_authenticator_register=dict( type="bool", aliases=["webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister"] ), web_authn_policy_passwordless_acceptable_aaguids=dict( type="list", elements="str", aliases=["webAuthnPolicyPasswordlessAcceptableAaguids"], no_log=False ), web_authn_policy_passwordless_extra_origins=dict( type="list", elements="str", aliases=["webAuthnPolicyPasswordlessExtraOrigins"], no_log=False ), web_authn_policy_passwordless_passkeys_enabled=dict( type="bool", aliases=["webAuthnPolicyPasswordlessPasskeysEnabled"] ), ) argument_spec.update(meta_args) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, required_one_of=( [ ["id", "realm", "enabled"], ["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="", diff={}, proposed={}, existing={}, end_state={}) # Obtain access token, initialize API try: connection_header = get_token(module.params) except KeycloakError as e: module.fail_json(msg=str(e)) kc = KeycloakAPI(module, connection_header) realm = module.params.get("realm") state = module.params.get("state") # convert module parameters to realm representation parameters (if they belong in there) params_to_ignore = list(keycloak_argument_spec().keys()) + ["state"] # Filter and map the parameters names that apply to the role realm_params = [x for x in module.params if x not in params_to_ignore and module.params.get(x) is not None] # See whether the realm already exists in Keycloak before_realm = kc.get_realm_by_id(realm=realm) if before_realm is None: before_realm = {} # Build a proposed changeset from parameters given to this module changeset = {} for realm_param in realm_params: new_param_value = module.params.get(realm_param) changeset[camel(realm_param)] = new_param_value # Prepare the desired values using the existing values (non-existence results in a dict that is save to use as a basis) desired_realm = before_realm.copy() desired_realm.update(changeset) result["proposed"] = sanitize_cr(changeset) before_realm_sanitized = sanitize_cr(before_realm) result["existing"] = before_realm_sanitized # Cater for when it doesn't exist (an empty dict) if not before_realm: if state == "absent": # Do nothing and exit if module._diff: result["diff"] = dict(before="", after="") result["changed"] = False result["end_state"] = {} result["msg"] = "Realm does not exist, doing nothing." module.exit_json(**result) # Process a creation result["changed"] = True if module._diff: result["diff"] = dict(before="", after=sanitize_cr(desired_realm)) if module.check_mode: module.exit_json(**result) # create it kc.create_realm(desired_realm) after_realm = kc.get_realm_by_id(desired_realm["realm"]) result["end_state"] = sanitize_cr(after_realm) result["msg"] = f"Realm {desired_realm['realm']} has been created." module.exit_json(**result) else: if state == "present": # Process an update # doing an update result["changed"] = True if module.check_mode: # We can only compare the current realm with the proposed updates we have before_norm = normalise_cr(before_realm) desired_norm = normalise_cr(desired_realm) if module._diff: result["diff"] = dict(before=sanitize_cr(before_norm), after=sanitize_cr(desired_norm)) result["changed"] = before_norm != desired_norm module.exit_json(**result) # do the update kc.update_realm(desired_realm, realm=realm) after_realm = kc.get_realm_by_id(realm=realm) if before_realm == after_realm: result["changed"] = False result["end_state"] = sanitize_cr(after_realm) if module._diff: result["diff"] = dict(before=before_realm_sanitized, after=sanitize_cr(after_realm)) result["msg"] = f"Realm {desired_realm['realm']} has been updated." module.exit_json(**result) else: # Process a deletion (because state was not 'present') result["changed"] = True if module._diff: result["diff"] = dict(before=before_realm_sanitized, after="") if module.check_mode: module.exit_json(**result) # delete it kc.delete_realm(realm=realm) result["proposed"] = {} result["end_state"] = {} result["msg"] = f"Realm {before_realm['realm']} has been deleted." module.exit_json(**result) if __name__ == "__main__": main()