mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-03-22 05:09:12 +00:00
keycloak_realm_key: add full support for all Keycloak key providers (#11468)
* feat(keycloak_realm_key): add support for auto-generated key providers Add support for Keycloak's auto-generated key providers where Keycloak manages the key material automatically: - rsa-generated: Auto-generates RSA signing keys - hmac-generated: Auto-generates HMAC signing keys - aes-generated: Auto-generates AES encryption keys - ecdsa-generated: Auto-generates ECDSA signing keys New algorithms: - HMAC: HS256, HS384, HS512 - ECDSA: ES256, ES384, ES512 - AES: AES (no algorithm parameter needed) New config options: - secret_size: For HMAC/AES providers (key size in bytes) - key_size: For RSA-generated provider (key size in bits) - elliptic_curve: For ECDSA-generated provider (P-256, P-384, P-521) Changes: - Make private_key/certificate optional (only required for rsa/rsa-enc) - Add provider-algorithm validation with clear error messages - Fix KeyError when managing default realm keys (issue #11459) - Maintain backward compatibility: RS256 default works for rsa/rsa-generated Fixes: #11459 * fix: address sanity test failures - Add 'default: RS256' to algorithm documentation to match spec - Add no_log=True to secret_size parameter per sanity check * feat(keycloak_realm_key): extend support for all Keycloak key providers Add support for remaining auto-generated key providers: - rsa-enc-generated (RSA encryption keys with RSA1_5, RSA-OAEP, RSA-OAEP-256) - ecdh-generated (ECDH key exchange with ECDH_ES, ECDH_ES_A128KW/A192KW/A256KW) - eddsa-generated (EdDSA signing with Ed25519, Ed448 curves) Changes: - Add provider-specific elliptic curve config key mapping (ecdsaEllipticCurveKey, ecdhEllipticCurveKey, eddsaEllipticCurveKey) - Add PROVIDERS_WITHOUT_ALGORITHM constant for providers that don't need algorithm - Add elliptic curve validation per provider type - Update documentation with all supported algorithms and examples - Add comprehensive integration tests for all new providers This completes full coverage of all Keycloak key provider types. * style: apply ruff formatting * feat(keycloak_realm_key): add java-keystore provider and update_password Add support for java-keystore provider to import keys from Java Keystore (JKS or PKCS12) files on the Keycloak server filesystem. Add update_password parameter to control password handling for java-keystore provider: - always (default): Always send passwords to Keycloak - on_create: Only send passwords when creating, preserve existing passwords when updating (enables idempotent playbooks) The on_create mode sends the masked value ("**********") that Keycloak recognizes as "preserve existing password", matching the behavior when re-importing an exported realm. Replace password_checksum with update_password - the checksum approach was complex and error-prone. The update_password parameter is simpler and follows the pattern used by ansible.builtin.user module. Also adds key_info return value containing kid, certificate fingerprint, status, and expiration for java-keystore keys. * address PR review feedback - Remove no_log=True from secret_size (just an int, not sensitive) - Add version_added: 12.4.0 to new parameters and return values - Remove "Added in community.general 12.4.0" from description text - Consolidate changelog entries into 4 focused entries - Remove bugfix from changelog (now in separate PR #11470) * address review feedback from russoz and felixfontein - remove docstrings from module-local helpers - remove line-by-line comments and unnecessary null guard - use specific exceptions instead of bare except Exception - use module.params["key"] instead of .get("key") - consolidate changelog into single entry - avoid "complete set" claim, reference Keycloak 26 instead * address round 2 review feedback - Extract remove_sensitive_config_keys() helper (DRY refactor) - Simplify RS256 validation to single code path - Add TypeError to inner except in compute_certificate_fingerprint() - Remove redundant comments (L812, L1031) - Switch .get() to direct dict access for module.params
This commit is contained in:
parent
5e0fd1201c
commit
80d21f2a0d
3 changed files with 1615 additions and 39 deletions
|
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- keycloak_realm_key - add support for auto-generated key providers (``rsa-generated``, ``rsa-enc-generated``, ``hmac-generated``, ``aes-generated``, ``ecdsa-generated``, ``ecdh-generated``, ``eddsa-generated``), ``java-keystore`` provider, additional algorithms (HMAC, ECDSA, ECDH, EdDSA, AES), and new config options (``secret_size``, ``key_size``, ``elliptic_curve``, ``keystore``, ``keystore_password``, ``key_alias``, ``key_password``). Also makes ``config.private_key`` and ``config.certificate`` optional as they are only required for imported key providers (https://github.com/ansible-collections/community.general/pull/11468).
|
||||||
|
|
@ -64,7 +64,23 @@ options:
|
||||||
description:
|
description:
|
||||||
- The name of the "provider ID" for the key.
|
- The name of the "provider ID" for the key.
|
||||||
- The value V(rsa-enc) has been added in community.general 8.2.0.
|
- The value V(rsa-enc) has been added in community.general 8.2.0.
|
||||||
choices: ['rsa', 'rsa-enc']
|
- The value V(java-keystore) has been added in community.general 12.4.0. This provider imports keys from
|
||||||
|
a Java Keystore (JKS or PKCS12) file located on the Keycloak server filesystem.
|
||||||
|
- The values V(rsa-generated), V(hmac-generated), V(aes-generated), and V(ecdsa-generated) have been added in
|
||||||
|
community.general 12.4.0. These are auto-generated key providers where Keycloak manages the key material.
|
||||||
|
- The values V(rsa-enc-generated), V(ecdh-generated), and V(eddsa-generated) have been added in
|
||||||
|
community.general 12.4.0. These correspond to the auto-generated key providers available in Keycloak 26.
|
||||||
|
choices:
|
||||||
|
- rsa
|
||||||
|
- rsa-enc
|
||||||
|
- java-keystore
|
||||||
|
- rsa-generated
|
||||||
|
- rsa-enc-generated
|
||||||
|
- hmac-generated
|
||||||
|
- aes-generated
|
||||||
|
- ecdsa-generated
|
||||||
|
- ecdh-generated
|
||||||
|
- eddsa-generated
|
||||||
default: 'rsa'
|
default: 'rsa'
|
||||||
type: str
|
type: str
|
||||||
config:
|
config:
|
||||||
|
|
@ -94,15 +110,48 @@ options:
|
||||||
- Key algorithm.
|
- Key algorithm.
|
||||||
- The values V(RS384), V(RS512), V(PS256), V(PS384), V(PS512), V(RSA1_5), V(RSA-OAEP), V(RSA-OAEP-256) have been
|
- The values V(RS384), V(RS512), V(PS256), V(PS384), V(PS512), V(RSA1_5), V(RSA-OAEP), V(RSA-OAEP-256) have been
|
||||||
added in community.general 8.2.0.
|
added in community.general 8.2.0.
|
||||||
|
- The values V(HS256), V(HS384), V(HS512) (for HMAC), V(ES256), V(ES384), V(ES512) (for ECDSA), and V(AES)
|
||||||
|
have been added in community.general 12.4.0.
|
||||||
|
- The values V(ECDH_ES), V(ECDH_ES_A128KW), V(ECDH_ES_A192KW), V(ECDH_ES_A256KW) (for ECDH key exchange),
|
||||||
|
and V(Ed25519), V(Ed448) (for EdDSA signing) have been added in community.general 12.4.0.
|
||||||
|
- For O(provider_id=rsa), O(provider_id=rsa-generated), and O(provider_id=java-keystore), defaults to V(RS256).
|
||||||
|
- For O(provider_id=rsa-enc) and O(provider_id=rsa-enc-generated), must be one of V(RSA1_5), V(RSA-OAEP), V(RSA-OAEP-256) (required, no default).
|
||||||
|
- For O(provider_id=hmac-generated), must be one of V(HS256), V(HS384), V(HS512) (required, no default).
|
||||||
|
- For O(provider_id=ecdsa-generated), must be one of V(ES256), V(ES384), V(ES512) (required, no default).
|
||||||
|
- For O(provider_id=ecdh-generated), must be one of V(ECDH_ES), V(ECDH_ES_A128KW), V(ECDH_ES_A192KW), V(ECDH_ES_A256KW) (required, no default).
|
||||||
|
- For O(provider_id=eddsa-generated), this option is not used (the algorithm is determined by O(config.elliptic_curve)).
|
||||||
|
- For O(provider_id=aes-generated), this option is not used (AES is always used).
|
||||||
|
choices:
|
||||||
|
- RS256
|
||||||
|
- RS384
|
||||||
|
- RS512
|
||||||
|
- PS256
|
||||||
|
- PS384
|
||||||
|
- PS512
|
||||||
|
- RSA1_5
|
||||||
|
- RSA-OAEP
|
||||||
|
- RSA-OAEP-256
|
||||||
|
- HS256
|
||||||
|
- HS384
|
||||||
|
- HS512
|
||||||
|
- ES256
|
||||||
|
- ES384
|
||||||
|
- ES512
|
||||||
|
- AES
|
||||||
|
- ECDH_ES
|
||||||
|
- ECDH_ES_A128KW
|
||||||
|
- ECDH_ES_A192KW
|
||||||
|
- ECDH_ES_A256KW
|
||||||
|
- Ed25519
|
||||||
|
- Ed448
|
||||||
default: RS256
|
default: RS256
|
||||||
choices: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'RSA1_5', 'RSA-OAEP', 'RSA-OAEP-256']
|
|
||||||
type: str
|
type: str
|
||||||
private_key:
|
private_key:
|
||||||
description:
|
description:
|
||||||
- The private key as an ASCII string. Contents of the key must match O(config.algorithm) and O(provider_id).
|
- The private key as an ASCII string. Contents of the key must match O(config.algorithm) and O(provider_id).
|
||||||
- Please note that the module cannot detect whether the private key specified differs from the current state's private
|
- Please note that the module cannot detect whether the private key specified differs from the current state's private
|
||||||
key. Use O(force=true) to force the module to update the private key if you expect it to be updated.
|
key. Use O(force=true) to force the module to update the private key if you expect it to be updated.
|
||||||
required: true
|
- Required when O(provider_id) is V(rsa) or V(rsa-enc). Not used for auto-generated providers.
|
||||||
type: str
|
type: str
|
||||||
certificate:
|
certificate:
|
||||||
description:
|
description:
|
||||||
|
|
@ -110,8 +159,71 @@ options:
|
||||||
and O(provider_id).
|
and O(provider_id).
|
||||||
- If you want Keycloak to automatically generate a certificate using your private key then set this to an empty
|
- If you want Keycloak to automatically generate a certificate using your private key then set this to an empty
|
||||||
string.
|
string.
|
||||||
required: true
|
- Required when O(provider_id) is V(rsa) or V(rsa-enc). Not used for auto-generated providers.
|
||||||
type: str
|
type: str
|
||||||
|
secret_size:
|
||||||
|
description:
|
||||||
|
- The size of the generated secret key in bytes.
|
||||||
|
- Only applicable to O(provider_id=hmac-generated) and O(provider_id=aes-generated).
|
||||||
|
- Valid values are V(16), V(24), V(32), V(64), V(128), V(256), V(512).
|
||||||
|
- Default is V(64) for HMAC, V(16) for AES.
|
||||||
|
type: int
|
||||||
|
version_added: 12.4.0
|
||||||
|
key_size:
|
||||||
|
description:
|
||||||
|
- The size of the generated key in bits.
|
||||||
|
- Only applicable to O(provider_id=rsa-generated) and O(provider_id=rsa-enc-generated).
|
||||||
|
- Valid values are V(1024), V(2048), V(4096). Default is V(2048).
|
||||||
|
type: int
|
||||||
|
version_added: 12.4.0
|
||||||
|
elliptic_curve:
|
||||||
|
description:
|
||||||
|
- The elliptic curve to use for ECDSA, ECDH, or EdDSA keys.
|
||||||
|
- For O(provider_id=ecdsa-generated) and O(provider_id=ecdh-generated), valid values are V(P-256), V(P-384), V(P-521). Default is V(P-256).
|
||||||
|
- For O(provider_id=eddsa-generated), valid values are V(Ed25519), V(Ed448). Default is V(Ed25519).
|
||||||
|
type: str
|
||||||
|
choices: ['P-256', 'P-384', 'P-521', 'Ed25519', 'Ed448']
|
||||||
|
version_added: 12.4.0
|
||||||
|
keystore:
|
||||||
|
description:
|
||||||
|
- Path to the Java Keystore file on the Keycloak server filesystem.
|
||||||
|
- Required when O(provider_id=java-keystore).
|
||||||
|
type: str
|
||||||
|
version_added: 12.4.0
|
||||||
|
keystore_password:
|
||||||
|
description:
|
||||||
|
- Password for the Java Keystore.
|
||||||
|
- Required when O(provider_id=java-keystore).
|
||||||
|
type: str
|
||||||
|
version_added: 12.4.0
|
||||||
|
key_alias:
|
||||||
|
description:
|
||||||
|
- Alias of the key within the keystore.
|
||||||
|
- Required when O(provider_id=java-keystore).
|
||||||
|
type: str
|
||||||
|
version_added: 12.4.0
|
||||||
|
key_password:
|
||||||
|
description:
|
||||||
|
- Password for the key within the keystore.
|
||||||
|
- If not specified, the O(config.keystore_password) is used.
|
||||||
|
- Only applicable to O(provider_id=java-keystore).
|
||||||
|
type: str
|
||||||
|
version_added: 12.4.0
|
||||||
|
update_password:
|
||||||
|
description:
|
||||||
|
- Controls when passwords are sent to Keycloak for V(java-keystore) provider.
|
||||||
|
- V(always) - Always send passwords. Keycloak will update the component even if passwords
|
||||||
|
have not changed. Use when you need to ensure passwords are updated.
|
||||||
|
- V(on_create) - Only send passwords when creating a new component. When updating an
|
||||||
|
existing component, send the masked value to preserve existing passwords. This makes
|
||||||
|
the module idempotent for password fields.
|
||||||
|
- This is necessary because Keycloak masks passwords in API responses (returns C(**********)),
|
||||||
|
making comparison impossible.
|
||||||
|
- Has no effect for providers other than V(java-keystore).
|
||||||
|
type: str
|
||||||
|
choices: ['always', 'on_create']
|
||||||
|
default: always
|
||||||
|
version_added: 12.4.0
|
||||||
notes:
|
notes:
|
||||||
- Current value of the private key cannot be fetched from Keycloak. Therefore comparing its desired state to the current
|
- Current value of the private key cannot be fetched from Keycloak. Therefore comparing its desired state to the current
|
||||||
state is not possible.
|
state is not possible.
|
||||||
|
|
@ -119,6 +231,12 @@ notes:
|
||||||
state of the certificate to the desired state (which may be empty) is not possible.
|
state of the certificate to the desired state (which may be empty) is not possible.
|
||||||
- Due to the private key and certificate options the module is B(not fully idempotent). You can use O(force=true) to force
|
- Due to the private key and certificate options the module is B(not fully idempotent). You can use O(force=true) to force
|
||||||
the module to ensure updating if you know that the private key might have changed.
|
the module to ensure updating if you know that the private key might have changed.
|
||||||
|
- For auto-generated providers (V(rsa-generated), V(rsa-enc-generated), V(hmac-generated), V(aes-generated), V(ecdsa-generated),
|
||||||
|
V(ecdh-generated), V(eddsa-generated)), Keycloak manages the key material automatically. The O(config.private_key) and
|
||||||
|
O(config.certificate) options are not used.
|
||||||
|
- For V(java-keystore) provider, the O(config.keystore_password) and O(config.key_password) values are returned masked by
|
||||||
|
Keycloak. Therefore comparing their current state to the desired state is not possible. Use O(update_password=on_create)
|
||||||
|
for idempotent playbooks, or use O(update_password=always) (default) if you need to ensure passwords are updated.
|
||||||
extends_documentation_fragment:
|
extends_documentation_fragment:
|
||||||
- community.general.keycloak
|
- community.general.keycloak
|
||||||
- community.general.keycloak.actiongroup_keycloak
|
- community.general.keycloak.actiongroup_keycloak
|
||||||
|
|
@ -146,6 +264,7 @@ EXAMPLES = r"""
|
||||||
active: true
|
active: true
|
||||||
priority: 120
|
priority: 120
|
||||||
algorithm: RS256
|
algorithm: RS256
|
||||||
|
|
||||||
- name: Manage Keycloak realm key and certificate
|
- name: Manage Keycloak realm key and certificate
|
||||||
community.general.keycloak_realm_key:
|
community.general.keycloak_realm_key:
|
||||||
name: custom
|
name: custom
|
||||||
|
|
@ -163,6 +282,178 @@ EXAMPLES = r"""
|
||||||
active: true
|
active: true
|
||||||
priority: 120
|
priority: 120
|
||||||
algorithm: RS256
|
algorithm: RS256
|
||||||
|
|
||||||
|
- name: Create HMAC signing key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: hmac-custom
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: hmac-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: HS256
|
||||||
|
secret_size: 64
|
||||||
|
|
||||||
|
- name: Create AES encryption key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: aes-custom
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: aes-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
secret_size: 16
|
||||||
|
|
||||||
|
- name: Create ECDSA signing key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: ecdsa-custom
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: ecdsa-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ES256
|
||||||
|
elliptic_curve: P-256
|
||||||
|
|
||||||
|
- name: Create RSA signing key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: rsa-auto
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: rsa-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
key_size: 2048
|
||||||
|
|
||||||
|
- name: Remove default HMAC key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: hmac-generated
|
||||||
|
state: absent
|
||||||
|
parent_id: myrealm
|
||||||
|
provider_id: hmac-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
|
||||||
|
- name: Create RSA encryption key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: rsa-enc-auto
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: rsa-enc-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RSA-OAEP
|
||||||
|
key_size: 2048
|
||||||
|
|
||||||
|
- name: Create ECDH key exchange key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: ecdh-custom
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: ecdh-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ECDH_ES
|
||||||
|
elliptic_curve: P-256
|
||||||
|
|
||||||
|
- name: Create EdDSA signing key (auto-generated)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: eddsa-custom
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: eddsa-generated
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
elliptic_curve: Ed25519
|
||||||
|
|
||||||
|
- name: Import key from Java Keystore (always update passwords)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: jks-imported
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: java-keystore
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
# update_password: always is the default - passwords are always sent to Keycloak
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: /opt/keycloak/conf/keystore.jks
|
||||||
|
keystore_password: "{{ keystore_password }}"
|
||||||
|
key_alias: mykey
|
||||||
|
key_password: "{{ key_password }}"
|
||||||
|
|
||||||
|
- name: Import key from Java Keystore (idempotent - only set password on create)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
name: jks-idempotent
|
||||||
|
state: present
|
||||||
|
parent_id: master
|
||||||
|
provider_id: java-keystore
|
||||||
|
auth_keycloak_url: http://localhost:8080/auth
|
||||||
|
auth_username: keycloak
|
||||||
|
auth_password: keycloak
|
||||||
|
auth_realm: master
|
||||||
|
update_password: on_create # Only send passwords when creating, preserve existing on update
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: /opt/keycloak/conf/keystore.jks
|
||||||
|
keystore_password: "{{ keystore_password }}"
|
||||||
|
key_alias: mykey
|
||||||
|
key_password: "{{ key_password }}"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN = r"""
|
RETURN = r"""
|
||||||
|
|
@ -219,8 +510,37 @@ end_state:
|
||||||
"140"
|
"140"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
key_info:
|
||||||
|
description:
|
||||||
|
- Cryptographic key metadata fetched from the realm keys endpoint.
|
||||||
|
- Only returned for V(java-keystore) provider when O(state=present) and not in check mode.
|
||||||
|
- This includes the key ID (kid) and certificate fingerprint, which can be used to detect
|
||||||
|
if the actual cryptographic key changed.
|
||||||
|
type: dict
|
||||||
|
returned: when O(provider_id=java-keystore) and O(state=present)
|
||||||
|
version_added: 12.4.0
|
||||||
|
contains:
|
||||||
|
kid:
|
||||||
|
description: The key ID (kid) - unique identifier for the cryptographic key.
|
||||||
|
type: str
|
||||||
|
sample: bN7p5Nc_V2M7N_-mb5vVSRVPKq5qD_OuARInB9ofsJ0
|
||||||
|
certificate_fingerprint:
|
||||||
|
description: SHA256 fingerprint of the certificate in colon-separated hex format.
|
||||||
|
type: str
|
||||||
|
sample: "A1:B2:C3:D4:E5:F6:..."
|
||||||
|
status:
|
||||||
|
description: The key status (ACTIVE, PASSIVE, DISABLED).
|
||||||
|
type: str
|
||||||
|
sample: ACTIVE
|
||||||
|
valid_to:
|
||||||
|
description: Certificate expiration timestamp in milliseconds since epoch.
|
||||||
|
type: int
|
||||||
|
sample: 1801789047000
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import hashlib
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
|
@ -234,6 +554,113 @@ from ansible_collections.community.general.plugins.module_utils.identity.keycloa
|
||||||
keycloak_argument_spec,
|
keycloak_argument_spec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Provider IDs that require private_key and certificate
|
||||||
|
IMPORTED_KEY_PROVIDERS = ["rsa", "rsa-enc"]
|
||||||
|
# Provider IDs that import keys from Java Keystore
|
||||||
|
KEYSTORE_PROVIDERS = ["java-keystore"]
|
||||||
|
# Provider IDs that auto-generate keys
|
||||||
|
GENERATED_KEY_PROVIDERS = [
|
||||||
|
"rsa-generated",
|
||||||
|
"rsa-enc-generated",
|
||||||
|
"hmac-generated",
|
||||||
|
"aes-generated",
|
||||||
|
"ecdsa-generated",
|
||||||
|
"ecdh-generated",
|
||||||
|
"eddsa-generated",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mapping of Ansible parameter names to Keycloak config property names
|
||||||
|
# for cases where camel() conversion doesn't produce the correct result.
|
||||||
|
# Each provider type may use a different config key for elliptic curve.
|
||||||
|
CONFIG_PARAM_MAPPING = {
|
||||||
|
"elliptic_curve": "ecdsaEllipticCurveKey",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Provider-specific config key names for elliptic_curve parameter
|
||||||
|
# ECDSA and ECDH both use the same curves (P-256, P-384, P-521) but different config keys
|
||||||
|
# EdDSA uses different curves (Ed25519, Ed448) with its own config key
|
||||||
|
ELLIPTIC_CURVE_CONFIG_KEYS = {
|
||||||
|
"ecdsa-generated": "ecdsaEllipticCurveKey",
|
||||||
|
"ecdh-generated": "ecdhEllipticCurveKey",
|
||||||
|
"eddsa-generated": "eddsaEllipticCurveKey",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Valid algorithm choices per provider type
|
||||||
|
# Note: aes-generated and eddsa-generated don't use algorithm config
|
||||||
|
PROVIDER_ALGORITHMS = {
|
||||||
|
"rsa": ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"],
|
||||||
|
"rsa-enc": ["RSA1_5", "RSA-OAEP", "RSA-OAEP-256"],
|
||||||
|
"java-keystore": ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"],
|
||||||
|
"rsa-generated": ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"],
|
||||||
|
"rsa-enc-generated": ["RSA1_5", "RSA-OAEP", "RSA-OAEP-256"],
|
||||||
|
"hmac-generated": ["HS256", "HS384", "HS512"],
|
||||||
|
"ecdsa-generated": ["ES256", "ES384", "ES512"],
|
||||||
|
"ecdh-generated": ["ECDH_ES", "ECDH_ES_A128KW", "ECDH_ES_A192KW", "ECDH_ES_A256KW"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Providers that don't use the algorithm config parameter
|
||||||
|
# eddsa-generated: algorithm is determined by the elliptic curve (Ed25519 or Ed448)
|
||||||
|
# aes-generated: always uses AES algorithm
|
||||||
|
PROVIDERS_WITHOUT_ALGORITHM = ["aes-generated", "eddsa-generated"]
|
||||||
|
|
||||||
|
# Providers where the RS256 default is valid (for backward compatibility)
|
||||||
|
PROVIDERS_WITH_RS256_DEFAULT = ["rsa", "rsa-generated", "java-keystore"]
|
||||||
|
|
||||||
|
# Config keys that cannot be compared and must be removed from changesets/diffs.
|
||||||
|
# privateKey/certificate: Keycloak doesn't return private keys, certificates are generated dynamically.
|
||||||
|
# keystorePassword/keyPassword: Keycloak masks these with "**********" in API responses.
|
||||||
|
SENSITIVE_CONFIG_KEYS = ["privateKey", "certificate", "keystorePassword", "keyPassword"]
|
||||||
|
|
||||||
|
|
||||||
|
def remove_sensitive_config_keys(config):
|
||||||
|
for key in SENSITIVE_CONFIG_KEYS:
|
||||||
|
config.pop(key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_keycloak_config_key(param_name, provider_id=None):
|
||||||
|
"""Convert Ansible parameter name to Keycloak config key.
|
||||||
|
|
||||||
|
Uses explicit mapping if available, otherwise applies camelCase conversion.
|
||||||
|
For elliptic_curve, the config key depends on the provider type.
|
||||||
|
"""
|
||||||
|
# Handle elliptic_curve specially - each provider uses a different config key
|
||||||
|
if param_name == "elliptic_curve" and provider_id in ELLIPTIC_CURVE_CONFIG_KEYS:
|
||||||
|
return ELLIPTIC_CURVE_CONFIG_KEYS[param_name]
|
||||||
|
if param_name in CONFIG_PARAM_MAPPING:
|
||||||
|
return CONFIG_PARAM_MAPPING[param_name]
|
||||||
|
return camel(param_name)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_certificate_fingerprint(certificate_pem):
|
||||||
|
try:
|
||||||
|
cert_der = base64.b64decode(certificate_pem)
|
||||||
|
fingerprint = hashlib.sha256(cert_der).hexdigest().upper()
|
||||||
|
return ":".join(fingerprint[i : i + 2] for i in range(0, len(fingerprint), 2))
|
||||||
|
except (ValueError, binascii.Error, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_key_info_for_component(kc, realm, component_id):
|
||||||
|
try:
|
||||||
|
keys_response = kc.get_realm_keys_metadata_by_id(realm)
|
||||||
|
if not keys_response or "keys" not in keys_response:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for key in keys_response.get("keys", []):
|
||||||
|
if key.get("providerId") == component_id:
|
||||||
|
return {
|
||||||
|
"kid": key.get("kid"),
|
||||||
|
"certificate_fingerprint": compute_certificate_fingerprint(key.get("certificate")),
|
||||||
|
"public_key": key.get("publicKey"),
|
||||||
|
"valid_to": key.get("validTo"),
|
||||||
|
"status": key.get("status"),
|
||||||
|
"algorithm": key.get("algorithm"),
|
||||||
|
"type": key.get("type"),
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""
|
"""
|
||||||
|
|
@ -248,7 +675,22 @@ def main():
|
||||||
name=dict(type="str", required=True),
|
name=dict(type="str", required=True),
|
||||||
force=dict(type="bool", default=False),
|
force=dict(type="bool", default=False),
|
||||||
parent_id=dict(type="str", required=True),
|
parent_id=dict(type="str", required=True),
|
||||||
provider_id=dict(type="str", default="rsa", choices=["rsa", "rsa-enc"]),
|
provider_id=dict(
|
||||||
|
type="str",
|
||||||
|
default="rsa",
|
||||||
|
choices=[
|
||||||
|
"rsa",
|
||||||
|
"rsa-enc",
|
||||||
|
"java-keystore",
|
||||||
|
"rsa-generated",
|
||||||
|
"rsa-enc-generated",
|
||||||
|
"hmac-generated",
|
||||||
|
"aes-generated",
|
||||||
|
"ecdsa-generated",
|
||||||
|
"ecdh-generated",
|
||||||
|
"eddsa-generated",
|
||||||
|
],
|
||||||
|
),
|
||||||
config=dict(
|
config=dict(
|
||||||
type="dict",
|
type="dict",
|
||||||
options=dict(
|
options=dict(
|
||||||
|
|
@ -268,12 +710,38 @@ def main():
|
||||||
"RSA1_5",
|
"RSA1_5",
|
||||||
"RSA-OAEP",
|
"RSA-OAEP",
|
||||||
"RSA-OAEP-256",
|
"RSA-OAEP-256",
|
||||||
|
"HS256",
|
||||||
|
"HS384",
|
||||||
|
"HS512",
|
||||||
|
"ES256",
|
||||||
|
"ES384",
|
||||||
|
"ES512",
|
||||||
|
"AES",
|
||||||
|
"ECDH_ES",
|
||||||
|
"ECDH_ES_A128KW",
|
||||||
|
"ECDH_ES_A192KW",
|
||||||
|
"ECDH_ES_A256KW",
|
||||||
|
"Ed25519",
|
||||||
|
"Ed448",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
private_key=dict(type="str", required=True, no_log=True),
|
private_key=dict(type="str", no_log=True),
|
||||||
certificate=dict(type="str", required=True),
|
certificate=dict(type="str"),
|
||||||
|
secret_size=dict(type="int", no_log=False),
|
||||||
|
key_size=dict(type="int"),
|
||||||
|
elliptic_curve=dict(type="str", choices=["P-256", "P-384", "P-521", "Ed25519", "Ed448"]),
|
||||||
|
keystore=dict(type="str", no_log=False),
|
||||||
|
keystore_password=dict(type="str", no_log=True),
|
||||||
|
key_alias=dict(type="str", no_log=False),
|
||||||
|
key_password=dict(type="str", no_log=True),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
update_password=dict(
|
||||||
|
type="str",
|
||||||
|
default="always",
|
||||||
|
choices=["always", "on_create"],
|
||||||
|
no_log=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
argument_spec.update(meta_args)
|
argument_spec.update(meta_args)
|
||||||
|
|
@ -288,8 +756,61 @@ def main():
|
||||||
required_by={"refresh_token": "auth_realm"},
|
required_by={"refresh_token": "auth_realm"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize the result object. Only "changed" seems to have special
|
provider_id = module.params["provider_id"]
|
||||||
# meaning for Ansible.
|
config = module.params["config"] or {}
|
||||||
|
state = module.params["state"]
|
||||||
|
|
||||||
|
# Validate that imported key providers have the required parameters
|
||||||
|
if state == "present" and provider_id in IMPORTED_KEY_PROVIDERS:
|
||||||
|
if not config.get("private_key"):
|
||||||
|
module.fail_json(msg=f"config.private_key is required for provider_id '{provider_id}'")
|
||||||
|
if config.get("certificate") is None:
|
||||||
|
module.fail_json(
|
||||||
|
msg=f"config.certificate is required for provider_id '{provider_id}' (use empty string for auto-generation)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate that java-keystore providers have the required parameters
|
||||||
|
if state == "present" and provider_id in KEYSTORE_PROVIDERS:
|
||||||
|
required_params = ["keystore", "keystore_password", "key_alias"]
|
||||||
|
missing = [p for p in required_params if not config.get(p)]
|
||||||
|
if missing:
|
||||||
|
module.fail_json(
|
||||||
|
msg=f"For provider_id=java-keystore, the following config options are required: {', '.join(missing)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate algorithm for providers that use it
|
||||||
|
if state == "present":
|
||||||
|
algorithm = config.get("algorithm")
|
||||||
|
if provider_id in PROVIDER_ALGORITHMS:
|
||||||
|
valid_algorithms = PROVIDER_ALGORITHMS[provider_id]
|
||||||
|
if algorithm not in valid_algorithms:
|
||||||
|
msg = f"algorithm '{algorithm}' is not valid for provider_id '{provider_id}'."
|
||||||
|
if algorithm == "RS256" and provider_id not in PROVIDERS_WITH_RS256_DEFAULT:
|
||||||
|
msg += " The default 'RS256' is not valid for this provider."
|
||||||
|
msg += f" Valid choices are: {', '.join(valid_algorithms)}"
|
||||||
|
module.fail_json(msg=msg)
|
||||||
|
elif provider_id in PROVIDERS_WITHOUT_ALGORITHM and algorithm is not None and algorithm != "RS256":
|
||||||
|
# aes-generated and eddsa-generated don't use algorithm - only warn if user explicitly set a non-default value
|
||||||
|
module.warn(f"algorithm is ignored for provider_id '{provider_id}'")
|
||||||
|
|
||||||
|
# Validate elliptic curve for providers that use it
|
||||||
|
if state == "present":
|
||||||
|
elliptic_curve = config.get("elliptic_curve")
|
||||||
|
if provider_id in ["ecdsa-generated", "ecdh-generated"] and elliptic_curve is not None:
|
||||||
|
valid_curves = ["P-256", "P-384", "P-521"]
|
||||||
|
if elliptic_curve not in valid_curves:
|
||||||
|
module.fail_json(
|
||||||
|
msg=f"elliptic_curve '{elliptic_curve}' is not valid for provider_id '{provider_id}'. "
|
||||||
|
f"Valid choices are: {', '.join(valid_curves)}"
|
||||||
|
)
|
||||||
|
elif provider_id == "eddsa-generated" and elliptic_curve is not None:
|
||||||
|
valid_curves = ["Ed25519", "Ed448"]
|
||||||
|
if elliptic_curve not in valid_curves:
|
||||||
|
module.fail_json(
|
||||||
|
msg=f"elliptic_curve '{elliptic_curve}' is not valid for provider_id '{provider_id}'. "
|
||||||
|
f"Valid choices are: {', '.join(valid_curves)}"
|
||||||
|
)
|
||||||
|
|
||||||
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
result = dict(changed=False, msg="", end_state={}, diff=dict(before={}, after={}))
|
||||||
|
|
||||||
# This will include the current state of the realm key if it is already
|
# This will include the current state of the realm key if it is already
|
||||||
|
|
@ -305,7 +826,7 @@ def main():
|
||||||
|
|
||||||
kc = KeycloakAPI(module, connection_header)
|
kc = KeycloakAPI(module, connection_header)
|
||||||
|
|
||||||
params_to_ignore = list(keycloak_argument_spec().keys()) + ["state", "force", "parent_id"]
|
params_to_ignore = list(keycloak_argument_spec().keys()) + ["state", "force", "parent_id", "update_password"]
|
||||||
|
|
||||||
# Filter and map the parameters names that apply to the role
|
# Filter and map the parameters names that apply to the role
|
||||||
component_params = [x for x in module.params if x not in params_to_ignore and module.params.get(x) is not None]
|
component_params = [x for x in module.params if x not in params_to_ignore and module.params.get(x) is not None]
|
||||||
|
|
@ -332,18 +853,25 @@ def main():
|
||||||
#
|
#
|
||||||
for component_param in component_params:
|
for component_param in component_params:
|
||||||
if component_param == "config":
|
if component_param == "config":
|
||||||
for config_param in module.params.get("config"):
|
for config_param in module.params["config"]:
|
||||||
changeset["config"][camel(config_param)] = []
|
raw_value = module.params["config"][config_param]
|
||||||
raw_value = module.params.get("config")[config_param]
|
# Optional params (secret_size, key_size, elliptic_curve) default to None.
|
||||||
|
# Skip them to avoid sending str(None) = "None" as a config value to Keycloak.
|
||||||
|
if raw_value is None:
|
||||||
|
continue
|
||||||
|
# Use custom mapping if available, otherwise camelCase
|
||||||
|
# Pass provider_id for elliptic_curve which uses different config keys per provider
|
||||||
|
keycloak_key = get_keycloak_config_key(config_param, provider_id)
|
||||||
|
changeset["config"][keycloak_key] = []
|
||||||
if isinstance(raw_value, bool):
|
if isinstance(raw_value, bool):
|
||||||
value = str(raw_value).lower()
|
value = str(raw_value).lower()
|
||||||
else:
|
else:
|
||||||
value = str(raw_value)
|
value = str(raw_value)
|
||||||
|
|
||||||
changeset["config"][camel(config_param)].append(value)
|
changeset["config"][keycloak_key].append(value)
|
||||||
else:
|
else:
|
||||||
# No need for camelcase in here as these are one word parameters
|
# No need for camelcase in here as these are one word parameters
|
||||||
new_param_value = module.params.get(component_param)
|
new_param_value = module.params[component_param]
|
||||||
changeset[camel(component_param)] = new_param_value
|
changeset[camel(component_param)] = new_param_value
|
||||||
|
|
||||||
# As provider_type is not a module parameter we have to add it to the
|
# As provider_type is not a module parameter we have to add it to the
|
||||||
|
|
@ -354,22 +882,14 @@ def main():
|
||||||
# changes to the current state.
|
# changes to the current state.
|
||||||
changeset_copy = deepcopy(changeset)
|
changeset_copy = deepcopy(changeset)
|
||||||
|
|
||||||
# It is not possible to compare current keys to desired keys, because the
|
# Remove keys that cannot be compared: privateKey/certificate (not returned
|
||||||
# certificate parameter is a base64-encoded binary blob created on the fly
|
# by Keycloak API) and keystore passwords (masked with "**********").
|
||||||
# when a key is added. Moreover, the Keycloak Admin API does not seem to
|
# The actual values remain in 'changeset' for the API payload.
|
||||||
# return the value of the private key for comparison. So, in effect, it we
|
remove_sensitive_config_keys(changeset_copy["config"])
|
||||||
# just have to ignore changes to the keys. However, as the privateKey
|
|
||||||
# parameter needs be present in the JSON payload, any changes done to any
|
|
||||||
# other parameters (e.g. config.priority) will trigger update of the keys
|
|
||||||
# as a side-effect.
|
|
||||||
del changeset_copy["config"]["privateKey"]
|
|
||||||
del changeset_copy["config"]["certificate"]
|
|
||||||
|
|
||||||
# Make it easier to refer to current module parameters
|
name = module.params["name"]
|
||||||
name = module.params.get("name")
|
force = module.params["force"]
|
||||||
force = module.params.get("force")
|
parent_id = module.params["parent_id"]
|
||||||
state = module.params.get("state")
|
|
||||||
parent_id = module.params.get("parent_id")
|
|
||||||
|
|
||||||
# Get a list of all Keycloak components that are of keyprovider type.
|
# Get a list of all Keycloak components that are of keyprovider type.
|
||||||
realm_keys = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
realm_keys = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
||||||
|
|
@ -407,7 +927,7 @@ def main():
|
||||||
# gracefully by using .get() with defaults.
|
# gracefully by using .get() with defaults.
|
||||||
for p, v in changeset_copy["config"].items():
|
for p, v in changeset_copy["config"].items():
|
||||||
# Get the current value, defaulting to our expected value if not present
|
# Get the current value, defaulting to our expected value if not present
|
||||||
# This handles the case where Keycloak does not return certain fields
|
# This handles the case where Keycloak doesn't return certain fields
|
||||||
# for default/generated keys
|
# for default/generated keys
|
||||||
current_value = key["config"].get(p, v)
|
current_value = key["config"].get(p, v)
|
||||||
before_realm_key["config"][p] = current_value
|
before_realm_key["config"][p] = current_value
|
||||||
|
|
@ -415,19 +935,45 @@ def main():
|
||||||
changes += f"config.{p}: {current_value} -> {v}, "
|
changes += f"config.{p}: {current_value} -> {v}, "
|
||||||
result["changed"] = True
|
result["changed"] = True
|
||||||
|
|
||||||
# Sanitize linefeeds for the privateKey. Without this the JSON payload
|
# For java-keystore provider, also fetch and compare key info (kid)
|
||||||
# will be invalid.
|
# This detects if the actual cryptographic key changed even when
|
||||||
|
# other config parameters remain the same
|
||||||
|
if provider_id in KEYSTORE_PROVIDERS:
|
||||||
|
current_key_info = get_key_info_for_component(kc, parent_id, key_id)
|
||||||
|
if current_key_info:
|
||||||
|
before_realm_key["key_info"] = {
|
||||||
|
"kid": current_key_info.get("kid"),
|
||||||
|
"certificate_fingerprint": current_key_info.get("certificate_fingerprint"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sanitize linefeeds for the privateKey and certificate (only for imported providers).
|
||||||
|
# Without this the JSON payload will be invalid.
|
||||||
|
if "privateKey" in changeset["config"]:
|
||||||
changeset["config"]["privateKey"][0] = changeset["config"]["privateKey"][0].replace("\\n", "\n")
|
changeset["config"]["privateKey"][0] = changeset["config"]["privateKey"][0].replace("\\n", "\n")
|
||||||
|
if "certificate" in changeset["config"]:
|
||||||
changeset["config"]["certificate"][0] = changeset["config"]["certificate"][0].replace("\\n", "\n")
|
changeset["config"]["certificate"][0] = changeset["config"]["certificate"][0].replace("\\n", "\n")
|
||||||
|
|
||||||
|
# For java-keystore provider: handle update_password parameter
|
||||||
|
# When update_password=on_create and we're updating an existing component,
|
||||||
|
# replace actual passwords with the masked value ("**********") that Keycloak
|
||||||
|
# returns in API responses. When Keycloak receives this masked value, it
|
||||||
|
# preserves the existing password instead of updating it.
|
||||||
|
# This makes the module idempotent for password fields.
|
||||||
|
update_password = module.params["update_password"]
|
||||||
|
if provider_id in KEYSTORE_PROVIDERS and key_id and update_password == "on_create":
|
||||||
|
SECRET_VALUE = "**********"
|
||||||
|
if "keystorePassword" in changeset["config"]:
|
||||||
|
changeset["config"]["keystorePassword"] = [SECRET_VALUE]
|
||||||
|
if "keyPassword" in changeset["config"]:
|
||||||
|
changeset["config"]["keyPassword"] = [SECRET_VALUE]
|
||||||
|
|
||||||
# Check all the possible states of the resource and do what is needed to
|
# Check all the possible states of the resource and do what is needed to
|
||||||
# converge current state with desired state (create, update or delete
|
# converge current state with desired state (create, update or delete
|
||||||
# the key).
|
# the key).
|
||||||
if key_id and state == "present":
|
if key_id and state == "present":
|
||||||
if result["changed"]:
|
if result["changed"]:
|
||||||
if module._diff:
|
if module._diff:
|
||||||
del before_realm_key["config"]["privateKey"]
|
remove_sensitive_config_keys(before_realm_key["config"])
|
||||||
del before_realm_key["config"]["certificate"]
|
|
||||||
result["diff"] = dict(before=before_realm_key, after=changeset_copy)
|
result["diff"] = dict(before=before_realm_key, after=changeset_copy)
|
||||||
|
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
|
|
@ -443,10 +989,26 @@ def main():
|
||||||
result["msg"] = f"Realm key {name} was in sync"
|
result["msg"] = f"Realm key {name} was in sync"
|
||||||
|
|
||||||
result["end_state"] = changeset_copy
|
result["end_state"] = changeset_copy
|
||||||
|
|
||||||
|
# For java-keystore provider, include key info in end_state
|
||||||
|
if provider_id in KEYSTORE_PROVIDERS:
|
||||||
|
if not module.check_mode:
|
||||||
|
key_info = get_key_info_for_component(kc, parent_id, key_id)
|
||||||
|
if key_info:
|
||||||
|
result["end_state"]["key_info"] = {
|
||||||
|
"kid": key_info.get("kid"),
|
||||||
|
"certificate_fingerprint": key_info.get("certificate_fingerprint"),
|
||||||
|
"status": key_info.get("status"),
|
||||||
|
"valid_to": key_info.get("valid_to"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
module.warn(
|
||||||
|
f"Key component '{name}' exists but no active key was found. "
|
||||||
|
"This may indicate an incorrect keystore password, path, or alias."
|
||||||
|
)
|
||||||
elif key_id and state == "absent":
|
elif key_id and state == "absent":
|
||||||
if module._diff:
|
if module._diff:
|
||||||
del before_realm_key["config"]["privateKey"]
|
remove_sensitive_config_keys(before_realm_key["config"])
|
||||||
del before_realm_key["config"]["certificate"]
|
|
||||||
result["diff"] = dict(before=before_realm_key, after={})
|
result["diff"] = dict(before=before_realm_key, after={})
|
||||||
|
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
|
|
@ -470,6 +1032,28 @@ def main():
|
||||||
result["changed"] = True
|
result["changed"] = True
|
||||||
result["msg"] = f"Realm key {name} created"
|
result["msg"] = f"Realm key {name} created"
|
||||||
|
|
||||||
|
# For java-keystore provider, fetch and include key info after creation
|
||||||
|
if provider_id in KEYSTORE_PROVIDERS:
|
||||||
|
# We need to get the component ID first (it was just created)
|
||||||
|
realm_keys_after = kc.get_components(urlencode(dict(type=provider_type)), parent_id)
|
||||||
|
for k in realm_keys_after:
|
||||||
|
if k["name"] == name:
|
||||||
|
new_key_id = k["id"]
|
||||||
|
key_info = get_key_info_for_component(kc, parent_id, new_key_id)
|
||||||
|
if key_info:
|
||||||
|
changeset_copy["key_info"] = {
|
||||||
|
"kid": key_info.get("kid"),
|
||||||
|
"certificate_fingerprint": key_info.get("certificate_fingerprint"),
|
||||||
|
"status": key_info.get("status"),
|
||||||
|
"valid_to": key_info.get("valid_to"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
module.warn(
|
||||||
|
f"Key component '{name}' was created but no active key was found. "
|
||||||
|
"This may indicate an incorrect keystore password, path, or alias."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
result["end_state"] = changeset_copy
|
result["end_state"] = changeset_copy
|
||||||
elif not key_id and state == "absent":
|
elif not key_id and state == "absent":
|
||||||
result["changed"] = False
|
result["changed"] = False
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,996 @@
|
||||||
- result.end_state.config.priority == ["150"]
|
- result.end_state.config.priority == ["150"]
|
||||||
- result.msg == "Realm key testkey_with_certificate was in sync"
|
- result.msg == "Realm key testkey_with_certificate was in sync"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Tests for auto-generated key providers
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create HMAC key (hmac-generated provider, check mode)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: HS256
|
||||||
|
secret_size: 64
|
||||||
|
check_mode: true
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert HMAC key would be created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "hmac-test-key"
|
||||||
|
- result.end_state.providerId == "hmac-generated"
|
||||||
|
- result.end_state.config.algorithm == ["HS256"]
|
||||||
|
- result.end_state.config.secretSize == ["64"]
|
||||||
|
- result.msg == "Realm key hmac-test-key would be created"
|
||||||
|
|
||||||
|
- name: Create HMAC key (hmac-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: HS256
|
||||||
|
secret_size: 64
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert HMAC key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "hmac-test-key"
|
||||||
|
- result.end_state.providerId == "hmac-generated"
|
||||||
|
- result.end_state.providerType == "org.keycloak.keys.KeyProvider"
|
||||||
|
- result.end_state.config.algorithm == ["HS256"]
|
||||||
|
- result.end_state.config.secretSize == ["64"]
|
||||||
|
- result.msg == "Realm key hmac-test-key created"
|
||||||
|
|
||||||
|
- name: Create HMAC key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: HS256
|
||||||
|
secret_size: 64
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert HMAC key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key hmac-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Update HMAC key priority
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 110
|
||||||
|
algorithm: HS256
|
||||||
|
secret_size: 64
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert HMAC key was updated
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state.config.priority == ["110"]
|
||||||
|
- "'config.priority' in result.msg"
|
||||||
|
|
||||||
|
- name: Remove HMAC key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
priority: 110
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert HMAC key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state == {}
|
||||||
|
- result.msg == "Realm key hmac-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# AES generated key tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create AES key (aes-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: aes-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: aes-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
secret_size: 32
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert AES key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "aes-test-key"
|
||||||
|
- result.end_state.providerId == "aes-generated"
|
||||||
|
- result.end_state.config.secretSize == ["32"]
|
||||||
|
- result.msg == "Realm key aes-test-key created"
|
||||||
|
|
||||||
|
- name: Create AES key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: aes-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: aes-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
secret_size: 32
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert AES key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key aes-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove AES key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: aes-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: aes-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert AES key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key aes-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ECDSA generated key tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create ECDSA key (ecdsa-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdsa-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ES256
|
||||||
|
elliptic_curve: P-256
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDSA key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "ecdsa-test-key"
|
||||||
|
- result.end_state.providerId == "ecdsa-generated"
|
||||||
|
- result.end_state.config.algorithm == ["ES256"]
|
||||||
|
- result.end_state.config.ecdsaEllipticCurveKey == ["P-256"]
|
||||||
|
- result.msg == "Realm key ecdsa-test-key created"
|
||||||
|
|
||||||
|
- name: Create ECDSA key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdsa-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ES256
|
||||||
|
elliptic_curve: P-256
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDSA key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key ecdsa-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove ECDSA key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdsa-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdsa-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDSA key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key ecdsa-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# RSA generated key tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create RSA generated key (rsa-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-gen-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
key_size: 2048
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA generated key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "rsa-gen-test-key"
|
||||||
|
- result.end_state.providerId == "rsa-generated"
|
||||||
|
- result.end_state.config.algorithm == ["RS256"]
|
||||||
|
- result.end_state.config.keySize == ["2048"]
|
||||||
|
- result.msg == "Realm key rsa-gen-test-key created"
|
||||||
|
|
||||||
|
- name: Create RSA generated key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-gen-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
key_size: 2048
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA generated key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key rsa-gen-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove RSA generated key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-gen-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA generated key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key rsa-gen-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Test managing default realm keys (issue #11459)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Update priority of default hmac-generated key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-generated
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 150
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert default hmac-generated key was updated
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state.config.priority == ["150"]
|
||||||
|
|
||||||
|
- name: Remove default hmac-generated key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: hmac-generated
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: hmac-generated
|
||||||
|
config:
|
||||||
|
priority: 150
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert default hmac-generated key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state == {}
|
||||||
|
- result.msg == "Realm key hmac-generated deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# RSA encryption generated key tests (rsa-enc-generated)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create RSA encryption key (rsa-enc-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-enc-gen-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-enc-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RSA-OAEP
|
||||||
|
key_size: 2048
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA encryption key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "rsa-enc-gen-test-key"
|
||||||
|
- result.end_state.providerId == "rsa-enc-generated"
|
||||||
|
- result.end_state.config.algorithm == ["RSA-OAEP"]
|
||||||
|
- result.end_state.config.keySize == ["2048"]
|
||||||
|
- result.msg == "Realm key rsa-enc-gen-test-key created"
|
||||||
|
|
||||||
|
- name: Create RSA encryption key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-enc-gen-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-enc-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RSA-OAEP
|
||||||
|
key_size: 2048
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA encryption key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key rsa-enc-gen-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove RSA encryption key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: rsa-enc-gen-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: rsa-enc-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert RSA encryption key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key rsa-enc-gen-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ECDH generated key tests (ecdh-generated)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create ECDH key (ecdh-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdh-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdh-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ECDH_ES
|
||||||
|
elliptic_curve: P-256
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDH key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "ecdh-test-key"
|
||||||
|
- result.end_state.providerId == "ecdh-generated"
|
||||||
|
- result.end_state.config.algorithm == ["ECDH_ES"]
|
||||||
|
- result.end_state.config.ecdhEllipticCurveKey == ["P-256"]
|
||||||
|
- result.msg == "Realm key ecdh-test-key created"
|
||||||
|
|
||||||
|
- name: Create ECDH key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdh-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdh-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: ECDH_ES
|
||||||
|
elliptic_curve: P-256
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDH key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key ecdh-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove ECDH key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: ecdh-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: ecdh-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert ECDH key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key ecdh-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# EdDSA generated key tests (eddsa-generated)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create EdDSA key (eddsa-generated provider)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: eddsa-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: eddsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
elliptic_curve: Ed25519
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert EdDSA key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "eddsa-test-key"
|
||||||
|
- result.end_state.providerId == "eddsa-generated"
|
||||||
|
- result.end_state.config.eddsaEllipticCurveKey == ["Ed25519"]
|
||||||
|
- result.msg == "Realm key eddsa-test-key created"
|
||||||
|
|
||||||
|
- name: Create EdDSA key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: eddsa-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: eddsa-generated
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
elliptic_curve: Ed25519
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert EdDSA key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key eddsa-test-key was in sync"
|
||||||
|
|
||||||
|
- name: Remove EdDSA key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: eddsa-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: eddsa-generated
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- name: Assert EdDSA key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.msg == "Realm key eddsa-test-key deleted"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Java Keystore provider tests (java-keystore)
|
||||||
|
# Note: These tests require a keystore file on the Keycloak server
|
||||||
|
# They are conditionally skipped if test_keystore_path is not defined
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create java-keystore key (check mode)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
check_mode: true
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key would be created (check mode)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "jks-test-key"
|
||||||
|
- result.end_state.providerId == "java-keystore"
|
||||||
|
- result.end_state.config.algorithm == ["RS256"]
|
||||||
|
- result.end_state.config.keystore == [test_keystore_path]
|
||||||
|
- result.end_state.config.keyAlias == [test_key_alias]
|
||||||
|
- result.msg == "Realm key jks-test-key would be created"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Create java-keystore key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "jks-test-key"
|
||||||
|
- result.end_state.providerId == "java-keystore"
|
||||||
|
- result.end_state.providerType == "org.keycloak.keys.KeyProvider"
|
||||||
|
- result.end_state.config.algorithm == ["RS256"]
|
||||||
|
- result.end_state.key_info is defined
|
||||||
|
- result.end_state.key_info.kid is defined
|
||||||
|
- result.end_state.key_info.certificate_fingerprint is defined
|
||||||
|
- result.end_state.key_info.status == "ACTIVE"
|
||||||
|
- result.msg == "Realm key jks-test-key created"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Create java-keystore key (test for idempotency)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key is in sync
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key jks-test-key was in sync"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Update java-keystore key priority
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-test-key
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 110
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key was updated
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state.config.priority == ["110"]
|
||||||
|
- "'config.priority' in result.msg"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Remove java-keystore key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-test-key
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
priority: 110
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state == {}
|
||||||
|
- result.msg == "Realm key jks-test-key deleted"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Java Keystore update_password tests
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
- name: Create java-keystore key with update_password=always (default)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
# update_password: always is the default
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "jks-update-pw-test"
|
||||||
|
- result.msg == "Realm key jks-update-pw-test created"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Re-run with update_password=always (should NOT be idempotent - passwords always sent)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
update_password: always
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
# Note: With update_password=always, the module always sends passwords to Keycloak.
|
||||||
|
# Keycloak doesn't report back if passwords changed, so the module reports "in sync"
|
||||||
|
# for the config comparison (passwords are excluded from comparison).
|
||||||
|
# The key difference is: always sends real passwords, on_create sends masked values.
|
||||||
|
- name: Assert java-keystore key is in sync (no config changes detected)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key jks-update-pw-test was in sync"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Remove java-keystore key to test update_password=on_create
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
priority: 100
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Create java-keystore key with update_password=on_create
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
update_password: on_create
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key was created with on_create mode
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state != {}
|
||||||
|
- result.end_state.name == "jks-update-pw-test"
|
||||||
|
- result.msg == "Realm key jks-update-pw-test created"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Re-run with update_password=on_create (should be idempotent)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
update_password: on_create
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 100
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore key is idempotent with on_create mode
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
- result.msg == "Realm key jks-update-pw-test was in sync"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Update priority with update_password=on_create (passwords preserved)
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: present
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
update_password: on_create
|
||||||
|
config:
|
||||||
|
enabled: true
|
||||||
|
active: true
|
||||||
|
priority: 110
|
||||||
|
algorithm: RS256
|
||||||
|
keystore: "{{ test_keystore_path }}"
|
||||||
|
keystore_password: "{{ test_keystore_password }}"
|
||||||
|
key_alias: "{{ test_key_alias }}"
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert priority was updated but passwords preserved
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state.config.priority == ["110"]
|
||||||
|
- "'config.priority' in result.msg"
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Remove java-keystore update_password test key
|
||||||
|
community.general.keycloak_realm_key:
|
||||||
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
auth_realm: "{{ admin_realm }}"
|
||||||
|
auth_username: "{{ admin_user }}"
|
||||||
|
auth_password: "{{ admin_password }}"
|
||||||
|
name: jks-update-pw-test
|
||||||
|
state: absent
|
||||||
|
parent_id: "{{ realm }}"
|
||||||
|
provider_id: java-keystore
|
||||||
|
config:
|
||||||
|
priority: 110
|
||||||
|
register: result
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
|
- name: Assert java-keystore update_password test key was deleted
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
- result.end_state == {}
|
||||||
|
when: test_keystore_path is defined
|
||||||
|
|
||||||
- name: Remove Keycloak test realm
|
- name: Remove Keycloak test realm
|
||||||
community.general.keycloak_realm:
|
community.general.keycloak_realm:
|
||||||
auth_keycloak_url: "{{ url }}"
|
auth_keycloak_url: "{{ url }}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue