Skip to content

Conversation

@rjeffman
Copy link
Member

@rjeffman rjeffman commented Jul 7, 2025

This is a proposed fix for #1333, including a new paskeyconfig module, and changes to ipahost, ipauser, ipaservice, and ipaconfig modules.

Please, see individual commits for details.

Summary by Sourcery

Enable passkey authentication across multiple FreeIPA Ansible modules by adding passkey choices, detecting support in test environment, updating tests, and introducing the new ipapasskeyconfig module with associated documentation.

New Features:

  • Introduce ipapasskeyconfig module to manage FreeIPA passkey configuration
  • Add 'passkey' as a supported authentication type in ipauser, ipahost, ipaservice, and ipaconfig modules

Enhancements:

  • Detect passkey support via env_freeipa_facts.yml to conditionally run tests

Documentation:

  • Update module READMEs to include 'passkey' in authentication choices
  • Add documentation and examples for ipapasskeyconfig module

Tests:

  • Extend existing user, host, service, and config tests to validate passkey support
  • Add dedicated tests for ipapasskeyconfig module

@rjeffman
Copy link
Member Author

rjeffman commented Jul 8, 2025

The failing tests in CentOS 8 require "guards" to not be executed as 'passkey' is not supported in this version.
This will require a new "freeipa fact", and the affected modules should also check for the availability of the 'passkey' value for the respective attributes.

@rjeffman rjeffman force-pushed the passkey_support branch 2 times, most recently from 35499be to 4a3975f Compare October 20, 2025 19:11
@rjeffman
Copy link
Member Author

@t-woerner the pylint errors are fixed by PR #1380.

@rjeffman rjeffman marked this pull request as ready for review October 20, 2025 19:15
@rjeffman rjeffman removed the DRAFT label Oct 20, 2025
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Centralize the supported auth types (including 'passkey') into a shared constant or helper instead of copying the literal choices array in each module to reduce duplication and maintenance overhead.
  • Extract the repeated passkey test blocks (ensure change, ensure idempotence, unsupported-message check) into a reusable include or test role to DRY up user/host/service/config test files.
  • The passkey support check in env_freeipa_facts.yml uses a grep on ipa command-find passkey output, which is brittle; consider using ipa_command_exists or inspecting the command return code directly for a more reliable feature detection.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Centralize the supported auth types (including 'passkey') into a shared constant or helper instead of copying the literal choices array in each module to reduce duplication and maintenance overhead.
- Extract the repeated passkey test blocks (ensure change, ensure idempotence, unsupported-message check) into a reusable include or test role to DRY up user/host/service/config test files.
- The passkey support check in env_freeipa_facts.yml uses a grep on `ipa command-find passkey` output, which is brittle; consider using ipa_command_exists or inspecting the command return code directly for a more reliable feature detection.

## Individual Comments

### Comment 1
<location> `plugins/modules/ipapasskeyconfig.py:104-115` </location>
<code_context>
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Consider removing or gating the debug flag for production use.

Enabling _ansible_debug in production can leak sensitive data or create noisy logs. Please ensure this is disabled or controlled via a parameter before merging.

```suggestion
            require_user_verification=dict(
                required=False, type='bool',
                aliases=["iparequireuserverification"],
                default=None
            ),
            debug=dict(
                required=False, type='bool',
                default=False
            ),
        ),
        supports_check_mode=True,
    )

    ansible_module._ansible_debug = ansible_module.params_get("debug")

    # Get parameters
```
</issue_to_address>

### Comment 2
<location> `plugins/modules/ipapasskeyconfig.py:82-90` </location>
<code_context>
+
+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
</code_context>

<issue_to_address>
**suggestion:** Consider handling unexpected exceptions in find_passkeyconfig.

Only ipalib_errors.NotFound is handled; consider catching or logging other possible exceptions to improve error visibility and diagnostics.

```suggestion
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
    except Exception as e:
        # Log unexpected exceptions for diagnostics
        module.fail_json(msg="Unexpected error in find_passkeyconfig: {}".format(e))
    return _result["result"]
```
</issue_to_address>

### Comment 3
<location> `plugins/modules/ipapasskeyconfig.py:127-129` </location>
<code_context>
+    # 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)
</code_context>

<issue_to_address>
**suggestion:** Failing early if passkeyconfig_show is unavailable is good, but consider surfacing the IPA version in the error message.

Displaying the IPA version in the error message will make it easier for users to identify and resolve compatibility problems.

```suggestion
        if not ansible_module.ipa_command_exists("passkeyconfig_show"):
            ipa_version = getattr(ansible_module.ipa, "version", "unknown")
            msg = f"Managing passkeyconfig is not supported by your IPA version ({ipa_version})"
            ansible_module.fail_json(msg=msg)
```
</issue_to_address>

### Comment 4
<location> `plugins/modules/ipapasskeyconfig.py:163-167` </location>
<code_context>
        if result:
            # Map IPA API field to module parameter
            if "iparequireuserverification" in result:
                exit_args["passkeyconfig"]["require_user_verification"] = \
                    result["iparequireuserverification"][0]

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
        if result and "iparequireuserverification" in result:
            exit_args["passkeyconfig"]["require_user_verification"] = \
                result["iparequireuserverification"][0]

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 5
<location> `plugins/modules/ipapasskeyconfig.py:151-156` </location>
<code_context>
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)

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Replace interpolated string formatting with f-string ([`replace-interpolation-with-fstring`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/replace-interpolation-with-fstring/))
- Remove redundant pass statement ([`remove-redundant-pass`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-pass/))

```suggestion
                        ansible_module.fail_json(msg=f"passkeyconfig_mod failed: {str(e)}")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@rjeffman rjeffman added the Candidate Good candidate for next minor release. label Oct 21, 2025
@rjeffman rjeffman changed the title WIP: Add support for passkey Add support for passkey Oct 21, 2025
@rjeffman rjeffman removed the Candidate Good candidate for next minor release. label Oct 21, 2025
@rjeffman
Copy link
Member Author

The certificate test failures are caused by the PKI update that changed the error message. This issue is addressed in another PR (#1392).

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 <[email protected]>
The value 'passkey' was missing as a valid value for user_auth_type
attribute.

Signed-off-by: Rafael Guterres Jeffman <[email protected]>
The value 'passkey' was missing as a valid value for auth_ind attribute.

Signed-off-by: Rafael Guterres Jeffman <[email protected]>
The value 'passkey' was missing as a valid value for auth_ind attribute.

Signed-off-by: Rafael Guterres Jeffman <[email protected]>
The value 'passkey' was missing as a valid value for user_auth_type
attribute.

Signed-off-by: Rafael Guterres Jeffman <[email protected]>
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-get.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 <[email protected]>
Copy link
Collaborator

@varunmylaraiah varunmylaraiah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All downstream test suites have successfully passed with this PR

@rjeffman
Copy link
Member Author

rjeffman commented Nov 17, 2025

@varun, did you include the Passkey MR which add more tests?

@rjeffman rjeffman added Candidate Good candidate for next minor release. and removed Downstream Tests labels Nov 27, 2025
@rjeffman
Copy link
Member Author

As downstream for new and changed modules also pass with this PR, I'm removing the "Downstream Tests" label, as tests are already in place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Candidate Good candidate for next minor release. Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ipauser does not support userauthtype: passkey

2 participants