Add Akeyless credential plugins, with OIDC workload identity support - #194
Add Akeyless credential plugins, with OIDC workload identity support#194kgal-akl wants to merge 2 commits into
Conversation
Add `akeyless` and `akeyless_ssh` credential plugins backed by the Akeyless SDK, covering static secret lookup (text, JSON and key-value formats) and signed SSH certificate issuance. Both authenticate with an Akeyless API key (access ID and access key) and support supplying a CA certificate for gateways that terminate TLS with a private CA. Type stubs for the `akeyless` SDK are vendored under `_type_stubs/` because the SDK does not ship any. Co-authored-by: Cursor <cursoragent@cursor.com>
Add `akeyless-oidc` and `akeyless-ssh-oidc`, which authenticate to Akeyless with the workload identity token issued by the controller instead of a stored API key, following the `hashivault-kv-oidc` / `hashivault-ssh-oidc` pattern. Both reuse the existing backends and differ only in their inputs: they expose the internal `workload_identity_token` field, drop `access_key`, and name the endpoint field `url`, since the controller reads the audience of the token it mints from the input of that exact name. Authentication now selects the auth material at call time, preferring a workload identity token (exchanged via the SDK's `jwt` access type) and falling back to the API key, so the API key plugins are unaffected. `access_id` on the OIDC types identifies the Akeyless JWT auth method that trusts the platform as an OIDC issuer, rather than an API key access ID. Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughAdds Akeyless credential plugins for static secrets, SSH certificates, and OIDC authentication, including typed SDK stubs, package entry points, optional dependencies, comprehensive tests, and supporting lint, typing, documentation, and environment configuration. ChangesAkeyless credential integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CredentialPlugin
participant AkeylessBackend
participant AkeylessAPI
participant AkeylessGateway
CredentialPlugin->>AkeylessBackend: provide credential inputs
AkeylessBackend->>AkeylessAPI: authenticate with access key or JWT
AkeylessAPI->>AkeylessGateway: send authentication request
AkeylessGateway-->>AkeylessAPI: return authentication token
AkeylessBackend->>AkeylessAPI: retrieve secret or request certificate
AkeylessAPI->>AkeylessGateway: execute credential operation
AkeylessGateway-->>AkeylessAPI: return secret or signed certificate
AkeylessAPI-->>AkeylessBackend: return operation result
AkeylessBackend-->>CredentialPlugin: return credential data
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/awx_plugins/credentials/akeyless.py (1)
476-503: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCollapse the duplicated
ApiException→RuntimeErrorwrapping.The same wrap block appears four times across both backends. A small context manager would remove the duplication and keep the message format in one place.
♻️ Suggested helper
import contextlib `@contextlib.contextmanager` def _wrapped_api_errors() -> _t.Iterator[None]: try: yield except _ApiException as api_exc: raise RuntimeError( f'Akeyless API error: {api_exc.reason}' f' (Status: {api_exc.status})', ) from api_exc except ValueError as val_err: raise RuntimeError(str(val_err)) from val_errwith _plugin.CertFiles(kwargs.get('ca_cert')) as ca_cert_path: api_instance = _setup_client( _resolve_gateway_url(kwargs), ca_cert_path, ) - try: + with _wrapped_api_errors(): token = _authenticate(api_instance, kwargs) - except _ApiException as api_exc: - ...Note this also normalizes the current asymmetry:
akeyless_backenddoes not wrapValueErrorfrom the fetch path whileakeyless_ssh_backenddoes.Also applies to: 536-566
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/awx_plugins/credentials/akeyless.py` around lines 476 - 503, Collapse the duplicated exception wrapping in a shared _wrapped_api_errors context manager, defining the Akeyless API and ValueError-to-RuntimeError conversions in one place. Use this context manager around both authentication and secret-fetch operations in akeyless_backend and the corresponding operations in akeyless_ssh_backend, preserving the existing error message format and normalizing ValueError handling for fetches.tests/akeyless_test.py (1)
27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTighten the
objectannotations so attribute access is type-checked.
_MockApiFactoryreturnsobjectyet callers domock_api.get_secret_value.side_effect = ..., andplugin: objectis followed byplugin.inputs[...]. Annotating asunittest.mock.Mockand_plugin.CredentialPluginrespectively keeps these assertions checkable without widening the module-level mypy suppressions at line 2.Also applies to: 463-463
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/akeyless_test.py` at line 27, Update the _MockApiFactory return annotation from object to unittest.mock.Mock, and change the plugin annotation at the referenced later usage from object to _plugin.CredentialPlugin. Preserve the existing behavior while enabling type checking for mock_api attribute access and plugin.inputs access without broadening mypy suppressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/awx_plugins/credentials/akeyless.py`:
- Around line 476-503: Collapse the duplicated exception wrapping in a shared
_wrapped_api_errors context manager, defining the Akeyless API and
ValueError-to-RuntimeError conversions in one place. Use this context manager
around both authentication and secret-fetch operations in akeyless_backend and
the corresponding operations in akeyless_ssh_backend, preserving the existing
error message format and normalizing ValueError handling for fetches.
In `@tests/akeyless_test.py`:
- Line 27: Update the _MockApiFactory return annotation from object to
unittest.mock.Mock, and change the plugin annotation at the referenced later
usage from object to _plugin.CredentialPlugin. Preserve the existing behavior
while enabling type checking for mock_api attribute access and plugin.inputs
access without broadening mypy suppressions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d1a45178-1944-4b53-9794-68ab47a0683e
📒 Files selected for processing (17)
.flake8.gitignore.mypy.ini.pre-commit-config.yaml.pylintrc.toml.ruff.toml_type_stubs/akeyless/__init__.pyi_type_stubs/akeyless/models/__init__.pyi_type_stubs/akeyless/models/get_ssh_certificate.pyi_type_stubs/akeyless/rest.pyidocs/conf.pydocs/spelling_wordlist.txtpyproject.tomlsrc/awx_plugins/credentials/akeyless.pytests/akeyless_test.pytests/importable_test.pytox.ini
Summary
Adds four Akeyless credential plugins:
akeylessakeyless_sshakeyless-oidcakeyless-ssh-oidcThe API key plugins cover static secret lookup (text, JSON and key-value formats) and signed SSH certificate issuance.
The OIDC plugins follow the
hashivault-kv-oidc/hashivault-ssh-oidcpattern: they reuse the same backends and differ only in their inputs, exposing the internalworkload_identity_tokenfield, droppingaccess_key, and naming the endpoint fieldurlsince the controller reads the audience of the minted token from the input of that exact name.access_idon those types identifies the Akeyless JWT auth method that trusts the platform as an OIDC issuer.Authentication selects the auth material at call time, preferring a workload identity token (exchanged via the SDK's
jwtaccess type) and falling back to the API key, so the API key plugins are unchanged.This supersedes #147, which could not be reopened.
Companion PR
The OIDC types also need ansible/awx#16562.
_is_oidc_namespace_disabled()filters against the hardcodedOIDC_CREDENTIAL_TYPE_NAMESPACESlist, so without that change these types stay visible whenFEATURE_OIDC_WORKLOAD_IDENTITY_ENABLEDis off and fail at job launch instead of being hidden.Test plan
workload_identity_tokenfield and the input namedurlpre-commitclean: flake8/wemake, pylint, mypy on 3.11/3.12/3.13awx-plugins-core0.1.dev3822+g5d80fc116):POST /api/v2/credentials/<id>/test/returns 202 with the expectedaudandaap_controller_*claimspasswordfrom Akeyless via the workload identity tokenakeyless_backend, confirming the lookup is in the execution pathMade with Cursor
Summary by CodeRabbit
New Features
Documentation
Tests