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'])