[AAPRFE-955] feat(credentials): add OAuth2 client_credentials token plugin#185
[AAPRFE-955] feat(credentials): add OAuth2 client_credentials token plugin#185amasolov wants to merge 14 commits into
Conversation
Add a new credential plugin that fetches a short-lived bearer token using the OAuth 2.0 client_credentials grant. This enables authentication with services that require OAuth2 tokens, such as Azure DevOps (via Microsoft Entra ID service principals), Keycloak, Okta, and any compliant OAuth2 identity provider. Closes ansible#184 Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a new OAuth2 Client Credentials credential plugin (implementation, entry point, optional dependency), tests, docs spelling updates, and a Sphinx nitpick ignore tweak. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller as AWX (credential resolver)
participant Plugin as oauth2_client_credentials_plugin
participant Backend as oauth2_client_credentials_backend
participant Requests as requests.post
participant Token as TokenEndpoint
Caller->>Plugin: Resolve credential
Plugin->>Backend: invoke backend(token_url, client_id, client_secret, scope)
Backend->>Requests: POST token_url with grant_type=client_credentials, client_id, client_secret[, scope]
Requests->>Token: HTTP request
Token-->>Requests: 200 + JSON { access_token } / non-200 or invalid JSON
Requests-->>Backend: HTTP response or raise Connection/Timeout error
alt success with access_token
Backend-->>Plugin: return access_token
Plugin-->>Caller: provide access token
else HTTP error / missing token / JSON error / network exception
Backend-->>Plugin: raise ValueError with details
Plugin-->>Caller: propagate error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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. Review rate limit: 0/1 reviews remaining, refill in 35 minutes and 41 seconds.Comment |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/awx_plugins/credentials/oauth2_client_credentials.py`:
- Around line 125-147: The code assumes resp.json() returns a mapping; defend
against non-object JSON by validating the parsed body is a dict before using
body.get(...) or indexing for ['access_token']. In the error path (where
resp.status_code != 200) call resp.json(), check isinstance(body, dict) and use
body.get('error_description', resp.text) only for dicts, otherwise set
error_detail to repr(body) or resp.text; similarly in the success path, parse =
resp.json(); if not isinstance(parse, dict) or 'access_token' not in parse,
raise the same ValueError with a clear message instead of letting
KeyError/TypeError leak (preserve the existing except-to-raise behavior around
access_token extraction).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: c8f9b075-3af0-49d2-83fa-5d713a438229
📒 Files selected for processing (5)
docs/spelling_wordlist.txtpyproject.tomlsrc/awx_plugins/credentials/oauth2_client_credentials.pytests/importable_test.pytests/oauth2_client_credentials_test.py
Guard resp.json() usage against token endpoints that return valid JSON that is not an object (e.g. an array). Previously this would raise an AttributeError or TypeError instead of the documented ValueError. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/oauth2_client_credentials_test.py (2)
193-213: ⚡ Quick winAssert the parametrized
token_urlis actually usedThis test currently proves token parsing, but not provider URL forwarding. If backend URL handling regresses (e.g., hardcoded URL), this case can still pass.
Suggested patch
def test_works_with_various_providers( mocker: MockerFixture, token_url: str, ) -> None: """Verify the plugin works with different OAuth2 provider URLs.""" - mocker.patch.object( + mock_post = mocker.patch.object( oauth2_mod.requests, 'post', return_value=_FakeResponse( status_code=200, json_data={'access_token': FAKE_TOKEN}, ), ) @@ token = oauth2_mod.oauth2_client_credentials_backend( token_url=token_url, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, ) assert token == FAKE_TOKEN + assert mock_post.call_args[0][0] == token_urlAs per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/oauth2_client_credentials_test.py` around lines 193 - 213, The test test_works_with_various_providers currently only validates token parsing; update it to assert that oauth2_mod.requests.post was invoked with the parametrized token_url so the backend actually forwards provider URLs. After patching oauth2_mod.requests.post (the mock used in the test), add an assertion on that mock (e.g., post_mock.assert_called_once_with(...) or check post_mock.call_args) to verify the first positional argument equals token_url and that expected form data/auth parameters used by oauth2_client_credentials_backend are present; reference the test name test_works_with_various_providers and the function oauth2_client_credentials_backend to locate where to add this assertion.
115-137: ⚡ Quick winLock in external-call contract assertions (
timeout, URL, andclient_secret)Given this backend calls an external token endpoint, this test should also guard against accidental removal of timeout/auth payload forwarding.
Suggested patch
call_kwargs = mock_post.call_args + assert call_kwargs[0][0] == TOKEN_URL + assert call_kwargs[1]['timeout'] == 30 assert call_kwargs[1]['data']['grant_type'] == 'client_credentials' assert call_kwargs[1]['data']['client_id'] == CLIENT_ID + assert call_kwargs[1]['data']['client_secret'] == CLIENT_SECRET assert call_kwargs[1]['data']['scope'] == ADO_SCOPEAs per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/oauth2_client_credentials_test.py` around lines 115 - 137, The test test_request_includes_grant_type is missing assertions that lock the external-call contract (timeout, URL and client_secret); update the test that mocks oauth2_mod.requests.post (mock_post) to also assert the request was made to TOKEN_URL, that call_kwargs[1] includes the forwarded client_secret value (CLIENT_SECRET) and that a timeout parameter is present and set (or at least non-None) so oauth2_client_credentials_backend continues to pass through timeout; locate these checks around the existing call_kwargs assertions and add assertions for URL, client_secret and timeout to prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@tests/oauth2_client_credentials_test.py`:
- Around line 193-213: The test test_works_with_various_providers currently only
validates token parsing; update it to assert that oauth2_mod.requests.post was
invoked with the parametrized token_url so the backend actually forwards
provider URLs. After patching oauth2_mod.requests.post (the mock used in the
test), add an assertion on that mock (e.g.,
post_mock.assert_called_once_with(...) or check post_mock.call_args) to verify
the first positional argument equals token_url and that expected form data/auth
parameters used by oauth2_client_credentials_backend are present; reference the
test name test_works_with_various_providers and the function
oauth2_client_credentials_backend to locate where to add this assertion.
- Around line 115-137: The test test_request_includes_grant_type is missing
assertions that lock the external-call contract (timeout, URL and
client_secret); update the test that mocks oauth2_mod.requests.post (mock_post)
to also assert the request was made to TOKEN_URL, that call_kwargs[1] includes
the forwarded client_secret value (CLIENT_SECRET) and that a timeout parameter
is present and set (or at least non-None) so oauth2_client_credentials_backend
continues to pass through timeout; locate these checks around the existing
call_kwargs assertions and add assertions for URL, client_secret and timeout to
prevent regressions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: 0e691348-15cc-44d3-bf89-e10bf100e018
📒 Files selected for processing (2)
src/awx_plugins/credentials/oauth2_client_credentials.pytests/oauth2_client_credentials_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/awx_plugins/credentials/oauth2_client_credentials.py
Extract _post_token_request, _extract_error_detail, and _extract_access_token helpers to reduce cognitive complexity, raise count, and try-body length. Replace magic number 30 with a named constant. Use http.HTTPStatus in tests to avoid WPS432 on status codes. Widen _FakeResponse.json_data to object to allow non-dict payloads in tests. Add noqa: ANN101 for self parameter annotations. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
ca44827 to
877d8d2
Compare
for more information, see https://pre-commit.ci
Annotate resp.json() return as object to satisfy mypy misc check. Move noqa: ANN101 to the self parameter line where flake8 reports it. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Add Okta and synchronisation to the spelling wordlist. Suppress Sphinx nitpick warning for requests.models.Response which is not in the intersphinx inventory. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Add test for the case where the token endpoint returns HTTP 200 but the response body is not valid JSON, exercising the ValueError re-raise in _extract_access_token. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Replace repeated oauth2_client_credentials_backend call arguments with a shared _COMMON_BACKEND_KWARGS dict to stay within the overused-expression limit. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Add a new managed credential type that fetches OAuth2 access tokens using the client_credentials grant. This enables external credential lookups against any OAuth2 provider (Microsoft Entra/Azure AD, Keycloak, Okta, etc.) for use in rulebook activations and other EDA workflows. Changes: - Register oauth2_client_credentials_plugin in PLUGIN_TYPES - Add DefaultCredentialType.OAUTH2_CLIENT_CREDENTIALS enum value - Define OAUTH2_CLIENT_CREDENTIALS_INPUTS schema and seed via create_initial_data management command - Add credentials-oauth2-client-credentials extra to awx-plugins-core dependency - Add unit tests for plugin registration, dispatch, and initial data seeding Depends-On: ansible/awx-plugins#185 Signed-off-by: Alexey Masolov <amasolov@redhat.com> Made-with: Cursor
Replace the mutable module-level dict constant with a pytest fixture that wraps the backend call, and consolidate error-path test cases into parametrized groups. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
for more information, see https://pre-commit.ci
…licit-any Use typing.Protocol for the fixture type to avoid Callable[..., str] which triggers mypy explicit-any. Bundle error test params into a NamedTuple to stay within the 5-argument limit. Flip negated condition to satisfy WPS504. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
3f44f95 to
0c2e324
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/awx_plugins/credentials/oauth2_client_credentials.py`:
- Around line 112-125: The current try/except around the requests.post call only
handles Timeout and ConnectionError but misses other transport failures; add a
final except requests.exceptions.RequestException as req_exc clause after the
existing handlers to normalize all remaining requests transport errors into a
ValueError (e.g., raise ValueError(f'Error requesting token from {token_url!s}')
from req_exc), keeping token_url, post_data and _REQUEST_TIMEOUT_SECONDS as used
in the existing requests.post call so every RequestException is wrapped and
propagated as a ValueError.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: 59dbf049-9c43-4935-a9a6-3ad2101adee6
📒 Files selected for processing (4)
docs/conf.pydocs/spelling_wordlist.txtsrc/awx_plugins/credentials/oauth2_client_credentials.pytests/oauth2_client_credentials_test.py
✅ Files skipped from review due to trivial changes (1)
- docs/conf.py
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/spelling_wordlist.txt
- tests/oauth2_client_credentials_test.py
Add a catch-all handler for requests.exceptions.RequestException after the specific Timeout and ConnectionError handlers to normalise all transport failures (e.g. TooManyRedirects, InvalidURL) to ValueError, matching the documented error contract. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Verify that test_works_with_various_providers checks the parametrized token_url is actually passed to requests.post, not just that a token is returned. Signed-off-by: Alexey Masolov <amasolov@redhat.com> Signed-off-by: Alexey Masolov <alexey.masolov@gmail.com> Made-with: Cursor
Resolves: AAPRFE-955
Summary
Add a new credential plugin (
oauth2_client_credentials) that fetches a short-lived bearer token using the OAuth 2.0client_credentialsgrant. This enables authentication with services that require OAuth2 tokens without storing long-lived credentials.Closes #184
How it works
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token)499b84ac-1321-427f-aa17-267ca6975798/.defaultfor Azure DevOps).access_tokenfrom the response.Works with any compliant OAuth2 provider including Microsoft Entra ID, Keycloak, Okta, and Auth0.
Changes
src/awx_plugins/credentials/oauth2_client_credentials.pytests/oauth2_client_credentials_test.pypyproject.tomlawx_plugins.credentialsand optional dependency groupcredentials-oauth2-client-credentialstests/importable_test.pydocs/spelling_wordlist.txtPrimary use case: Azure DevOps project sync
Azure DevOps has deprecated PATs and SSH keys for service-to-service authentication in favour of Microsoft Entra ID service principals. This plugin enables:
x-token, scope:499b84ac-1321-427f-aa17-267ca6975798/.default).Testing
pytest+pytest-mock).Signed-off-by: Alexey Masolov amasolov@redhat.com
Made with Cursor
Summary by CodeRabbit
New Features
Tests
Documentation