From ff1a026ef48b76449800b10d211e337535a152b3 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Mon, 20 Oct 2025 16:02:57 -0300 Subject: [PATCH 1/6] tests: Add fact for passkey support When testing passkey attributes some version of IPA do not support it, se we need a fact that states that the support is available for proper testing. Signed-off-by: Rafael Guterres Jeffman --- tests/env_freeipa_facts.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/env_freeipa_facts.yml b/tests/env_freeipa_facts.yml index bb8fb674..67ec415b 100644 --- a/tests/env_freeipa_facts.yml +++ b/tests/env_freeipa_facts.yml @@ -38,12 +38,24 @@ krb5ccname: "__check_ipa_host_is_client_or_server__" register: check_ad_support +- name: Verify if passkey tests are possible + ansible.builtin.shell: + cmd: | + echo SomeADMINpassword | kinit -c {{ krb5ccname }} admin > /dev/null + RESULT=$(KRB5CCNAME={{ krb5ccname }} ipa command-find passkey | grep "Number of entries returned") + kdestroy -A -c {{ krb5ccname }} > /dev/null + echo $RESULT + vars: + krb5ccname: "__check_ipa_host_is_client_or_server__" + register: check_passkey_support + - name: Set FreeIPA facts. ansible.builtin.set_fact: ipa_version: "{{ ipa_cmd_version.stdout_lines[0] }}" ipa_api_version: "{{ ipa_cmd_version.stdout_lines[1] }}" ipa_host_is_client: "{{ (check_client.stdout_lines[-1] == 'CLIENT') | bool }}" trust_test_is_supported: "{{ 'AD trust agent' in check_ad_support.stdout }}" + passkey_is_supported: "{{ 'Number of entries returned 0' not in check_passkey_support.stdout }}" - name: Ensure ipaserver_domain is set when: ipaserver_domain is not defined From a733c031b08331958c95fcf87316382c2fa44819 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Fri, 4 Jul 2025 17:43:18 -0300 Subject: [PATCH 2/6] ipaconfig: Add support for 'passkey' in 'user_auth_type' The value 'passkey' was missing as a valid value for user_auth_type attribute. Signed-off-by: Rafael Guterres Jeffman --- README-config.md | 2 +- plugins/modules/ipaconfig.py | 4 +- .../test_config_empty_string_params.yml | 38 +++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/README-config.md b/README-config.md index a57c7399..2ba92b17 100644 --- a/README-config.md +++ b/README-config.md @@ -145,7 +145,7 @@ Variable | Description | Required `selinuxusermaporder` \| `ipaselinuxusermaporder`| Set ordered list in increasing priority of SELinux users | no `selinuxusermapdefault`\| `ipaselinuxusermapdefault` | Set default SELinux user when no match is found in SELinux map rule | no `pac_type` \| `ipakrbauthzdata` | set default types of PAC supported for services (choices: `MS-PAC`, `PAD`, `nfs:NONE`). Use `""` to clear this variable. | no -`user_auth_type` \| `ipauserauthtype` | set default types of supported user authentication (choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `disabled`, `""`). An additional check ensures that only types can be used that are supported by the IPA version. Use `""` to clear this variable. | no +`user_auth_type` \| `ipauserauthtype` | set default types of supported user authentication (choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `passkey`, `disabled`, `""`). An additional check ensures that only types can be used that are supported by the IPA version. Use `""` to clear this variable. | no `domain_resolution_order` \| `ipadomainresolutionorder` | Set list of domains used for short name qualification | no `ca_renewal_master_server` \| `ipacarenewalmasterserver`| Renewal master for IPA certificate authority. | no `enable_sid` | New users and groups automatically get a SID assigned. Cannot be deactivated once activated. Requires IPA 4.9.8+. (bool) | no diff --git a/plugins/modules/ipaconfig.py b/plugins/modules/ipaconfig.py index c80da429..8cd6854d 100644 --- a/plugins/modules/ipaconfig.py +++ b/plugins/modules/ipaconfig.py @@ -161,7 +161,7 @@ options: type: list elements: str choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", - "disabled", ""] + "passkey", "disabled", ""] aliases: ["ipauserauthtype"] ca_renewal_master_server: description: Renewal master for IPA certificate authority. @@ -426,7 +426,7 @@ def main(): user_auth_type=dict(type="list", elements="str", required=False, choices=["password", "radius", "otp", "pkinit", "hardened", "idp", - "disabled", ""], + "passkey", "disabled", ""], aliases=["ipauserauthtype"]), ca_renewal_master_server=dict(type="str", required=False), domain_resolution_order=dict(type="list", elements="str", diff --git a/tests/config/test_config_empty_string_params.yml b/tests/config/test_config_empty_string_params.yml index 5329c203..09e0e200 100644 --- a/tests/config/test_config_empty_string_params.yml +++ b/tests/config/test_config_empty_string_params.yml @@ -5,6 +5,8 @@ gather_facts: no tasks: + - name: Include tasks ../env_freeipa_facts.yml + ansible.builtin.include_tasks: ../env_freeipa_facts.yml # GET CURRENT CONFIG @@ -80,6 +82,36 @@ register: result failed_when: result.changed or result.failed + - name: Ensure config with user_auth_type passkey + ipaconfig: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + user_auth_type: + - passkey + register: result + failed_when: not result.changed or result.failed + when: passkey_is_supported + + - name: Ensure config with user_auth_type passkey, again + ipaconfig: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + user_auth_type: + - passkey + register: result + failed_when: result.changed or result.failed + when: passkey_is_supported + + - name: Check if correct message is given if passkey is not supported. + ipaconfig: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + user_auth_type: + - passkey + register: result + failed_when: not result.failed or "'passkey' is not supported" not in result.msg + when: not passkey_is_supported + - name: Ensure config with empty user_auth_type ipaconfig: ipaadmin_password: SomeADMINpassword @@ -138,6 +170,6 @@ ipaconfig: ipaadmin_password: SomeADMINpassword ipaapi_context: "{{ ipa_context | default(omit) }}" - pac_type: '{{ previousconfig.config.pac_type }}' - user_auth_type: '{{ previousconfig.config.user_auth_type }}' - configstring: '{{ previousconfig.config.configstring }}' + pac_type: '{{ previousconfig.config.pac_type | default("") }}' + user_auth_type: '{{ previousconfig.config.user_auth_type | default("") }}' + configstring: '{{ previousconfig.config.configstring | default("") }}' From 1488fb7b5eee07eec4e1cc6dfc19e6f712272810 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Fri, 4 Jul 2025 17:59:05 -0300 Subject: [PATCH 3/6] ipahost: Add support for 'passkey' in 'auth_ind' The value 'passkey' was missing as a valid value for auth_ind attribute. Signed-off-by: Rafael Guterres Jeffman --- README-host.md | 2 +- plugins/modules/ipahost.py | 6 ++-- ...rams.yml => test_host_auth_ind_params.yml} | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) rename tests/host/{test_host_empty_string_params.yml => test_host_auth_ind_params.yml} (67%) diff --git a/README-host.md b/README-host.md index eb278a6b..70107f7b 100644 --- a/README-host.md +++ b/README-host.md @@ -354,7 +354,7 @@ Variable | Description | Required `mac_address` \| `macaddress` | List of hardware MAC addresses. | no `sshpubkey` \| `ipasshpubkey` | List of SSH public keys | no `userclass` \| `class` | Host category (semantics placed on this attribute are for local interpretation) | no -`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. Use 'otp' to allow OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA authentications. Use empty string to reset auth_ind to the initial value. Other values may be used for custom configurations. An additional check ensures that only types can be used that are supported by the IPA version. Choices: ["radius", "otp", "pkinit", "hardened", "idp", ""] | no +`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. Use 'otp' to allow OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA authentications. Use empty string to reset auth_ind to the initial value. Other values may be used for custom configurations. An additional check ensures that only types can be used that are supported by the IPA version. Choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""] | no `requires_pre_auth` \| `ipakrbrequirespreauth` | Pre-authentication is required for the service (bool) | no `ok_as_delegate` \| `ipakrbokasdelegate` | Client credentials may be delegated to the service (bool) | no `ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client (bool) | no diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py index 24acaa36..aa353ec9 100644 --- a/plugins/modules/ipahost.py +++ b/plugins/modules/ipahost.py @@ -184,7 +184,7 @@ options: type: list elements: str aliases: ["krbprincipalauthind"] - choices: ["radius", "otp", "pkinit", "hardened", "idp", ""] + choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""] required: false requires_pre_auth: description: Pre-authentication is required for the service @@ -356,7 +356,7 @@ options: type: list elements: str aliases: ["krbprincipalauthind"] - choices: ["radius", "otp", "pkinit", "hardened", "idp", ""] + choices: ["radius", "otp", "pkinit", "hardened", "idp", "passkey", ""] required: false requires_pre_auth: description: Pre-authentication is required for the service @@ -758,7 +758,7 @@ def main(): auth_ind=dict(type='list', elements="str", aliases=["krbprincipalauthind"], default=None, choices=["radius", "otp", "pkinit", "hardened", "idp", - ""]), + "passkey", ""]), requires_pre_auth=dict(type="bool", aliases=["ipakrbrequirespreauth"], default=None), ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"], diff --git a/tests/host/test_host_empty_string_params.yml b/tests/host/test_host_auth_ind_params.yml similarity index 67% rename from tests/host/test_host_empty_string_params.yml rename to tests/host/test_host_auth_ind_params.yml index 1e30f5e6..be061f37 100644 --- a/tests/host/test_host_empty_string_params.yml +++ b/tests/host/test_host_auth_ind_params.yml @@ -5,6 +5,9 @@ gather_facts: yes tasks: + - name: Include FreeIPA facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + - name: Get Domain from server name ansible.builtin.set_fact: ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join('.') }}" @@ -58,6 +61,39 @@ register: result failed_when: result.changed or result.failed + - name: Ensure host "{{ host1_fqdn }}" present with auth_ind passkey + ipahost: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: "{{ host1_fqdn }}" + auth_ind: + - passkey + register: result + when: passkey_is_supported + failed_when: not result.changed or result.failed + + - name: Ensure host "{{ host1_fqdn }}" present with auth_ind passkey, again + ipahost: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: "{{ host1_fqdn }}" + auth_ind: + - passkey + register: result + when: passkey_is_supported + failed_when: result.changed or result.failed + + - name: Check if correct message is given if passkey is not supported. + ipahost: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: "{{ host1_fqdn }}" + auth_ind: + - passkey + register: result + when: not passkey_is_supported + failed_when: not result.failed or "'passkey' is not supported" not in result.msg + - name: Ensure host "{{ host1_fqdn }}" present with empty auth_ind ipahost: ipaadmin_password: SomeADMINpassword From 17b100baecbfc7eb4c8c012cc49dd163bdbd6e2d Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Fri, 4 Jul 2025 18:11:19 -0300 Subject: [PATCH 4/6] ipaservice: Add support for 'passkey' in 'auth_ind' The value 'passkey' was missing as a valid value for auth_ind attribute. Signed-off-by: Rafael Guterres Jeffman --- README-service.md | 2 +- plugins/modules/ipaservice.py | 6 ++-- .../test_service_empty_string_params.yml | 33 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/README-service.md b/README-service.md index afdd1248..9144bd01 100644 --- a/README-service.md +++ b/README-service.md @@ -361,7 +361,7 @@ Variable | Description | Required -------- | ----------- | -------- `certificate` \| `usercertificate` | Base-64 encoded service certificate. | no `pac_type` \| `ipakrbauthzdata` | Supported PAC type. It can be one of `MS-PAC`, `PAD`, or `NONE`. Use empty string to reset pac_type to the initial value. | no -`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. It can be any of `otp`, `radius`, `pkinit`, `hardened`, `idp` or `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset auth_ind to the initial value. | no +`auth_ind` \| `krbprincipalauthind` | Defines an allow list for Authentication Indicators. It can be any of `otp`, `radius`, `pkinit`, `hardened`, `idp`, `passkey` or `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset auth_ind to the initial value. | no `requires_pre_auth` \| `ipakrbrequirespreauth` | Pre-authentication is required for the service. Default to true. (bool) | no `ok_as_delegate` \| `ipakrbokasdelegate` | Client credentials may be delegated to the service. Default to false. (bool) | no `ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client. Default to false. (bool) | no diff --git a/plugins/modules/ipaservice.py b/plugins/modules/ipaservice.py index 1e270b5e..7d6b1992 100644 --- a/plugins/modules/ipaservice.py +++ b/plugins/modules/ipaservice.py @@ -74,7 +74,7 @@ options: type: list elements: str required: false - choices: ["otp", "radius", "pkinit", "hardened", "idp", ""] + choices: ["otp", "radius", "pkinit", "hardened", "idp", "passkey", ""] aliases: ["krbprincipalauthind"] skip_host_check: description: Skip checking if host object exists. @@ -192,7 +192,7 @@ options: type: list elements: str required: false - choices: ["otp", "radius", "pkinit", "hardened", "idp", ""] + choices: ["otp", "radius", "pkinit", "hardened", "idp", "passkey", ""] aliases: ["krbprincipalauthind"] skip_host_check: description: Skip checking if host object exists. @@ -560,7 +560,7 @@ def init_ansible_module(): auth_ind=dict(type="list", elements="str", aliases=["krbprincipalauthind"], choices=["otp", "radius", "pkinit", "hardened", "idp", - ""]), + "passkey", ""]), skip_host_check=dict(type="bool"), force=dict(type="bool"), requires_pre_auth=dict( diff --git a/tests/service/test_service_empty_string_params.yml b/tests/service/test_service_empty_string_params.yml index 1831e496..1d7af6ec 100644 --- a/tests/service/test_service_empty_string_params.yml +++ b/tests/service/test_service_empty_string_params.yml @@ -5,6 +5,8 @@ gather_facts: yes tasks: + - name: Include tasks ../env_freeipa_facts.yml + ansible.builtin.include_tasks: ../env_freeipa_facts.yml # CLEANUP TEST ITEMS @@ -83,6 +85,37 @@ register: result failed_when: result.changed or result.failed + - name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with auth_ind passkey + ipaservice: + ipaadmin_password: SomeADMINpassword + name: "test-service/{{ ansible_facts['fqdn'] }}" + auth_ind: + - passkey + register: result + failed_when: not result.changed or result.failed + when: passkey_is_supported + + - name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with auth_ind passkey, again + ipaservice: + ipaadmin_password: SomeADMINpassword + name: "test-service/{{ ansible_facts['fqdn'] }}" + auth_ind: + - passkey + register: result + failed_when: result.changed or result.failed + when: passkey_is_supported + + - name: Check if correct message is given if passkey is not supported. + ipaservice: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: "test-service/{{ ansible_facts['fqdn'] }}" + auth_ind: + - passkey + register: result + failed_when: not result.failed or "'passkey' is not supported" not in result.msg + when: not passkey_is_supported + - name: Ensure service "test-service/{{ ansible_facts['fqdn'] }}" is present with empty auth_ind ipaservice: ipaadmin_password: SomeADMINpassword From 536b7cb5f3abc422380c881a138f2fb6d26ed228 Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Fri, 4 Jul 2025 18:14:48 -0300 Subject: [PATCH 5/6] ipauser: Add support for 'passkey' in 'user_auth_type' The value 'passkey' was missing as a valid value for user_auth_type attribute. Signed-off-by: Rafael Guterres Jeffman --- README-user.md | 2 +- plugins/modules/ipauser.py | 8 +++++--- tests/user/test_user.yml | 39 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/README-user.md b/README-user.md index f9ee2ae0..1c45a0bd 100644 --- a/README-user.md +++ b/README-user.md @@ -452,7 +452,7 @@ Variable | Description | Required `manager` | List of manager user names. | no `carlicense` | List of car licenses. | no `sshpubkey` \| `ipasshpubkey` | List of SSH public keys. | no -`userauthtype` \| `ipauserauthtype` | List of supported user authentication types. Choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp` and `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset userauthtype to the initial value. | no +`userauthtype` \| `ipauserauthtype` | List of supported user authentication types. Choices: `password`, `radius`, `otp`, `pkinit`, `hardened`, `idp`, `passkey` and `""`. An additional check ensures that only types can be used that are supported by the IPA version. Use empty string to reset userauthtype to the initial value. | no `userclass` | User category. (semantics placed on this attribute are for local interpretation). | no `radius` | RADIUS proxy configuration | no `radiususer` | RADIUS proxy username | no diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py index d16d2c0c..7c57f996 100644 --- a/plugins/modules/ipauser.py +++ b/plugins/modules/ipauser.py @@ -208,7 +208,8 @@ options: Use empty string to reset userauthtype to the initial value. type: list elements: str - choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", ""] + choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", + "passkey", ""] required: false aliases: ["ipauserauthtype"] userclass: @@ -480,7 +481,8 @@ options: Use empty string to reset userauthtype to the initial value. type: list elements: str - choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", ""] + choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", + "passkey", ""] required: false aliases: ["ipauserauthtype"] userclass: @@ -1070,7 +1072,7 @@ def main(): userauthtype=dict(type='list', elements="str", aliases=["ipauserauthtype"], default=None, choices=["password", "radius", "otp", "pkinit", - "hardened", "idp", ""]), + "hardened", "idp", "passkey", ""]), userclass=dict(type="list", elements="str", aliases=["class"], default=None), radius=dict(type="str", aliases=["ipatokenradiusconfiglink"], diff --git a/tests/user/test_user.yml b/tests/user/test_user.yml index daad3e74..67fb3165 100644 --- a/tests/user/test_user.yml +++ b/tests/user/test_user.yml @@ -5,6 +5,9 @@ gather_facts: false tasks: + - name: Include FreeIPA facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + - name: Remove test users ipauser: ipaadmin_password: SomeADMINpassword @@ -392,6 +395,42 @@ register: result failed_when: not result.changed or result.failed + - name: Ensure user pinky with userauthtype passkey exists + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: pinky + first: pinky + last: user + userauthtype: passkey + register: result + failed_when: not result.changed or result.failed + when: passkey_is_supported + + - name: Ensure user pinky with userauthtype passkey exists, again + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: pinky + first: pinky + last: user + userauthtype: passkey + register: result + failed_when: result.changed or result.failed + when: passkey_is_supported + + - name: Check if correct message is given if passkey is not supported. + ipauser: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + name: pinky + first: pinky + last: user + userauthtype: passkey + register: result + when: not passkey_is_supported + failed_when: not result.failed or "'passkey' is not supported" not in result.msg + - name: User pinky absent and preserved for future exclusion. ipauser: ipaadmin_password: SomeADMINpassword From bf384ab1aa9cd0fc003a388ec5861ff05503996d Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Mon, 7 Jul 2025 15:50:57 -0300 Subject: [PATCH 6/6] New passkeyconfig management module There is a new paskeyconfig management module placed in the plugins folder: plugins/modules/ipapasskeyconfig.py The paskeyconfig module allows to retrieve and modify global passkey configuration attributes. Here is the documentation of the module: README-passkeyconfig.md New example playbooks have been added: playbooks/passkeyconfig/passkeyconfig-retrieve.yml playbooks/passkeyconfig/passkeyconfig-present.yml New tests for the module can be found at: tests/passkeyconfig/test_passkeyconfig.yml tests/passkeyconfig/test_passkeyconfig_client_context.yml Signed-off-by: Rafael Guterres Jeffman --- README-passkeyconfig.md | 88 +++++++++ README.md | 2 + .../passkeyconfig/passkeyconfig-present.yml | 10 + .../passkeyconfig/passkeyconfig-retrieve.yml | 14 ++ plugins/modules/ipapasskeyconfig.py | 174 ++++++++++++++++++ tests/passkeyconfig/test_passkeyconfig.yml | 67 +++++++ .../test_passkeyconfig_client_context.yml | 40 ++++ 7 files changed, 395 insertions(+) create mode 100644 README-passkeyconfig.md create mode 100644 playbooks/passkeyconfig/passkeyconfig-present.yml create mode 100644 playbooks/passkeyconfig/passkeyconfig-retrieve.yml create mode 100644 plugins/modules/ipapasskeyconfig.py create mode 100644 tests/passkeyconfig/test_passkeyconfig.yml create mode 100644 tests/passkeyconfig/test_passkeyconfig_client_context.yml diff --git a/README-passkeyconfig.md b/README-passkeyconfig.md new file mode 100644 index 00000000..0b4073f7 --- /dev/null +++ b/README-passkeyconfig.md @@ -0,0 +1,88 @@ +Passkeyconfig module +============ + +Description +----------- + +The passkeyconfig module allows to manage FreeIPA passkey configuration settings. + +Features +-------- + +* Passkeyconfig management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipapasskeyconfig module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.15+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +By default, user verification for passkey authentication is turned on (`true`). Example playbook to ensure that the requirement for user verification for passkey authentication is turned off: + +```yaml +--- +- name: Playbook to manage IPA passkeyconfig. + hosts: ipaserver + become: false + + tasks: + - name: Ensure require_user_verification is false + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + require_user_verification: false +``` + + +Example playbook to get current passkeyconfig: + +```yaml +--- +- name: Playbook to get IPA passkeyconfig. + hosts: ipaserver + become: false + + tasks: + - name: Retrieve current passkey configuration + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword +``` + + +Variables +--------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no +`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to true. (bool) | no +`require_user_verification` \| `iparequireuserverification` | Require user verification for passkey authentication. (bool) | no + + +Authors +======= + +Rafael Guterres Jeffman diff --git a/README.md b/README.md index 7558b23a..eb6c9412 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Features * Modules for idview management * Modules for location management * Modules for netgroup management +* Modules for passkeyconfig management * Modules for permission management * Modules for privilege management * Modules for pwpolicy management @@ -454,6 +455,7 @@ Modules in plugin/modules * [idview](README-idview.md) * [ipalocation](README-location.md) * [ipanetgroup](README-netgroup.md) +* [ipapasskeyconfig](README-passkeyconfig.md) * [ipapermission](README-permission.md) * [ipaprivilege](README-privilege.md) * [ipapwpolicy](README-pwpolicy.md) diff --git a/playbooks/passkeyconfig/passkeyconfig-present.yml b/playbooks/passkeyconfig/passkeyconfig-present.yml new file mode 100644 index 00000000..41362633 --- /dev/null +++ b/playbooks/passkeyconfig/passkeyconfig-present.yml @@ -0,0 +1,10 @@ +--- +- name: Passkeyconfig example + hosts: ipaserver + become: no + + tasks: + - name: Set passkeyconfig require_user_verification to false + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + require_user_verification: false diff --git a/playbooks/passkeyconfig/passkeyconfig-retrieve.yml b/playbooks/passkeyconfig/passkeyconfig-retrieve.yml new file mode 100644 index 00000000..41e0e56f --- /dev/null +++ b/playbooks/passkeyconfig/passkeyconfig-retrieve.yml @@ -0,0 +1,14 @@ +--- +- name: Passkeyconfig get current configuration example + hosts: ipaserver + become: true + + tasks: + - name: Get current passkey configuration + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + register: result + + - name: Display current passkey configuration + ansible.builtin.debug: + var: result.passkeyconfig diff --git a/plugins/modules/ipapasskeyconfig.py b/plugins/modules/ipapasskeyconfig.py new file mode 100644 index 00000000..94bd36e7 --- /dev/null +++ b/plugins/modules/ipapasskeyconfig.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +# Authors: +# Rafael Guterres Jeffman +# +# Copyright (C) 2025 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipapasskeyconfig +short_description: Manage FreeIPA passkeyconfig +description: Manage FreeIPA passkeyconfig +extends_documentation_fragment: + - ipamodule_base_docs +options: + require_user_verification: + description: Require user verification for passkey authentication + required: false + type: bool + default: true + aliases: ["iparequireuserverification"] +author: + - Rafael Guterres Jeffman (@rjeffman) +""" + +EXAMPLES = """ +# Set passkeyconfig +- ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + require_user_verification: false + +# Get current passkeyconfig +- ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword +""" + +RETURN = """ +passkeyconfig: + description: Dict of passkeyconfig settings + returned: always + type: dict + contains: + require_user_verification: + description: Require user verification for passkey authentication + type: bool + returned: always +""" + + +from ansible.module_utils.ansible_freeipa_module import \ + IPAAnsibleModule, compare_args_ipa, ipalib_errors +from ansible.module_utils import six + +if six.PY3: + unicode = str + + +def find_passkeyconfig(module): + """Find the current passkeyconfig settings.""" + try: + _result = module.ipa_command_no_name( + "passkeyconfig_show", {"all": True}) + except ipalib_errors.NotFound: + # An exception is raised if passkeyconfig is not found. + return None + return _result["result"] + + +def gen_args(require_user_verification): + _args = {} + if require_user_verification is not None: + _args["iparequireuserverification"] = require_user_verification + return _args + + +def main(): + ansible_module = IPAAnsibleModule( + argument_spec=dict( + # passkeyconfig + require_user_verification=dict( + required=False, type='bool', + aliases=["iparequireuserverification"], + default=None + ), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + require_user_verification = ( + ansible_module.params_get("require_user_verification") + ) + + # Init + changed = False + exit_args = {} + + # Connect to IPA API + with ansible_module.ipa_connect(): + + if not ansible_module.ipa_command_exists("passkeyconfig_show"): + msg = "Managing passkeyconfig is not supported by your IPA version" + ansible_module.fail_json(msg=msg) + + result = find_passkeyconfig(ansible_module) + + if result is None: + ansible_module.fail_json(msg="Could not retrieve passkeyconfig") + + if require_user_verification is not None: + # Generate args + args = gen_args(require_user_verification) + + # Check if there are different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, result): + changed = True + if not ansible_module.check_mode: + try: + ansible_module.ipa_command_no_name( + "passkeyconfig_mod", args) + except ipalib_errors.EmptyModlist: + changed = False + except Exception as e: + ansible_module.fail_json( + msg="passkeyconfig_mod failed: %s" % str(e)) + else: + # No parameters provided, just return current config + pass + + # Get updated config if changes were made + if changed: + result = find_passkeyconfig(ansible_module) + + # Prepare exit args + exit_args["passkeyconfig"] = {} + if result: + # Map IPA API field to module parameter + if "iparequireuserverification" in result: + exit_args["passkeyconfig"]["require_user_verification"] = \ + result["iparequireuserverification"][0] + + # Done + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/tests/passkeyconfig/test_passkeyconfig.yml b/tests/passkeyconfig/test_passkeyconfig.yml new file mode 100644 index 00000000..91943b43 --- /dev/null +++ b/tests/passkeyconfig/test_passkeyconfig.yml @@ -0,0 +1,67 @@ +--- +- name: Test passkeyconfig + hosts: "{{ ipa_test_host | default('ipaserver') }}" + # It is normally not needed to set "become" to "true" for a module test. + # Only set it to true if it is needed to execute commands as root. + become: false + # Enable "gather_facts" only if "ansible_facts" variable needs to be used. + gather_facts: false + module_defaults: + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + ipaapi_context: "{{ ipa_context | default(omit) }}" + + tasks: + + - name: Include FreeIPA facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + + - name: Run tests only if passkey is supported + when: passkey_is_supported + block: + # TESTS + + - name: Get current passkeyconfig + ipapasskeyconfig: + register: result_initial + failed_when: result_initial.failed + + - name: Ensure require_user_verification is set to false + ipapasskeyconfig: + require_user_verification: false + register: result + failed_when: result.failed + + - name: Ensure require_user_verification is set to false again + ipapasskeyconfig: + require_user_verification: false + register: result + failed_when: result.changed or result.failed + + - name: Verify require_user_verification is false + ansible.builtin.assert: + that: + - result.passkeyconfig.require_user_verification == false + + - name: Ensure require_user_verification is set to true + ipapasskeyconfig: + require_user_verification: true + register: result + failed_when: not result.changed or result.failed + + - name: Ensure require_user_verification is set to true again + ipapasskeyconfig: + require_user_verification: true + register: result + failed_when: result.changed or result.failed + + - name: Verify require_user_verification is true + ansible.builtin.assert: + that: + - result.passkeyconfig.require_user_verification == true + + # CLEANUP: Restore original configuration + - name: Restore original passkeyconfig + ipapasskeyconfig: + require_user_verification: "{{ result_initial.passkeyconfig.require_user_verification }}" + when: result_initial.passkeyconfig is defined and result_initial.passkeyconfig.require_user_verification is defined diff --git a/tests/passkeyconfig/test_passkeyconfig_client_context.yml b/tests/passkeyconfig/test_passkeyconfig_client_context.yml new file mode 100644 index 00000000..ec518583 --- /dev/null +++ b/tests/passkeyconfig/test_passkeyconfig_client_context.yml @@ -0,0 +1,40 @@ +--- +- name: Test passkeyconfig + hosts: ipaclients, ipaserver + # It is normally not needed to set "become" to "true" for a module test. + # Only set it to true if it is needed to execute commands as root. + become: false + # Enable "gather_facts" only if "ansible_facts" variable needs to be used. + gather_facts: false + + tasks: + - name: Include FreeIPA facts. + ansible.builtin.include_tasks: ../env_freeipa_facts.yml + + # Test will only be executed if host is not a server. + - name: Execute with server context in the client. + ipapasskeyconfig: + ipaadmin_password: SomeADMINpassword + ipaapi_context: server + require_user_verification: false + register: result + failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*")) + when: ipa_host_is_client and passkey_is_supported + +# Import basic module tests, and execute with ipa_context set to 'client'. +# If ipaclients is set, it will be executed using the client, if not, +# ipaserver will be used. +# +# With this setup, tests can be executed against an IPA client, against +# an IPA server using "client" context, and ensure that tests are executed +# in upstream CI. + +- name: Test passkeyconfig using client context, in client host. + import_playbook: test_passkeyconfig.yml + when: groups['ipaclients'] and passkey_is_supported + vars: + ipa_test_host: ipaclients + +- name: Test passkeyconfig using client context, in server host. + import_playbook: test_passkeyconfig.yml + when: passkey_is_supported and (groups['ipaclients'] is not defined or not groups['ipaclients'])