Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ awx-plugins-core = { version = "^0.0.1a10", extras = [
"credentials-centrify-vault-kv",
"credentials-conjur",
"credentials-github-app",
"credentials-oauth2-client-credentials",
"credentials-hashivault-kv",
"credentials-hashivault-ssh",
"credentials-thycotic-dsv",
Expand Down
1 change: 1 addition & 0 deletions src/aap_eda/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class DefaultCredentialType(DjangoStrEnum):
CYBERARK_CONJUR = "CyberArk Conjur Secrets Manager Lookup"
MSFT_AZURE_VAULT = "Microsoft Azure Key Vault"
GITHUB_APP = "GitHub App Installation Access Token Lookup"
OAUTH2_CLIENT_CREDENTIALS = "OAuth2 Client Credentials Token Lookup"


# TODO: rename to "RulebookProcessStatus" or "ParentProcessStatus"
Expand Down
53 changes: 53 additions & 0 deletions src/aap_eda/core/management/commands/create_initial_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,51 @@
"required": ["app_or_client_id", "install_id", "private_rsa_key"],
}

OAUTH2_CLIENT_CREDENTIALS_INPUTS = {
"fields": [
{
"id": "token_url",
"label": "Token Endpoint URL",
"type": "string",
"help_text": (
"The full OAuth2 token endpoint URL. "
"For Microsoft Entra ID: "
"https://login.microsoftonline.com/"
"<TENANT_ID>/oauth2/v2.0/token"
),
},
{
"id": "client_id",
"label": "Client ID",
"type": "string",
"help_text": (
"The OAuth2 client identifier "
"(Application ID for Microsoft Entra)."
),
},
{
"id": "client_secret",
"label": "Client Secret",
"type": "string",
"secret": True,
"help_text": "The OAuth2 client secret.",
},
],
"metadata": [
{
"id": "scope",
"label": "Scope (optional)",
"type": "string",
"help_text": (
"The OAuth2 scope to request. "
"For Azure DevOps: "
"499b84ac-1321-427f-aa17-267ca6975798/.default"
),
},
],
"required": ["token_url", "client_id", "client_secret"],
}

EVENT_STREAM_MTLS_INPUTS = {
"fields": [
{
Expand Down Expand Up @@ -2314,6 +2359,14 @@
"injectors": {},
"managed": True,
},
{
"name": enums.DefaultCredentialType.OAUTH2_CLIENT_CREDENTIALS,
"namespace": "oauth2_client_credentials",
"inputs": OAUTH2_CLIENT_CREDENTIALS_INPUTS,
"kind": "external",
"injectors": {},
"managed": True,
},
{
"name": enums.EventStreamCredentialType.MTLS,
"namespace": "event_stream",
Expand Down
4 changes: 4 additions & 0 deletions src/aap_eda/core/utils/credential_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
hashivault_kv_plugin,
hashivault_ssh_plugin,
)
from awx_plugins.credentials.oauth2 import (
oauth2_client_credentials_plugin,
)
from awx_plugins.credentials.tss import tss_plugin

from aap_eda.core.exceptions import (
Expand All @@ -44,6 +47,7 @@
"github_app": github_app_lookup,
"hashivault_kv": hashivault_kv_plugin,
"hashivault_ssh": hashivault_ssh_plugin,
"oauth2_client_credentials": oauth2_client_credentials_plugin,
"thycotic_dsv": dsv_plugin,
"thycotic_tss": tss_plugin,
}
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/commands/test_create_initial_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from aap_eda.core import enums, models
from aap_eda.core.management.commands.create_initial_data import (
OAUTH2_CLIENT_CREDENTIALS_INPUTS,
POSTGRES_CREDENTIAL_INPUTS,
)
from aap_eda.core.utils.credentials import inputs_from_store
Expand Down Expand Up @@ -379,3 +380,28 @@ def test_create_initial_data_rule_engine_cred_with_rootcert():
decoded_inputs = inputs_from_store(raw_inputs)

assert "rule-engine-rootcert" in decoded_inputs["postgres_sslrootcert"]


@pytest.mark.django_db
def test_create_initial_data_oauth2_client_credentials_type():
"""Test that the OAuth2 Client Credentials credential type is seeded."""
call_command("create_initial_data")
cred_type = models.CredentialType.objects.get(
name=enums.DefaultCredentialType.OAUTH2_CLIENT_CREDENTIALS
)
assert cred_type.managed is True
assert cred_type.kind == "external"
assert cred_type.namespace == "oauth2_client_credentials"
assert cred_type.inputs == OAUTH2_CLIENT_CREDENTIALS_INPUTS
assert cred_type.injectors == {}

field_ids = [f["id"] for f in cred_type.inputs["fields"]]
assert "token_url" in field_ids
assert "client_id" in field_ids
assert "client_secret" in field_ids

secret_field = next(
f for f in cred_type.inputs["fields"]
if f["id"] == "client_secret"
)
assert secret_field["secret"] is True
1 change: 1 addition & 0 deletions tests/unit/test_credential_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"github_app",
"hashivault_kv",
"hashivault_ssh",
"oauth2_client_credentials",
"thycotic_dsv",
"thycotic_tss",
],
Expand Down
148 changes: 148 additions & 0 deletions tests/unit/test_oauth2_credential_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Tests for the OAuth2 Client Credentials credential plugin in EDA."""
# Copyright 2026 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from unittest import mock

import pytest
import responses
from awx_plugins.credentials.plugin import CredentialPlugin

from aap_eda.core.enums import DefaultCredentialType
from aap_eda.core.utils.credential_plugins import (
PLUGIN_TYPES,
run_plugin,
)


TOKEN_URL = (
"https://login.microsoftonline.com/"
"00000000-0000-0000-0000-000000000000/oauth2/v2.0/token"
)
CLIENT_ID = "11111111-1111-1111-1111-111111111111"
CLIENT_SECRET = "test-secret-value"
FAKE_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.fake.token"


class TestOAuth2PluginRegistration:
"""Verify the plugin is wired into the EDA plugin registry."""

def test_plugin_key_in_plugin_types(self):
assert "oauth2_client_credentials" in PLUGIN_TYPES

def test_plugin_is_credential_plugin_instance(self):
plugin = PLUGIN_TYPES["oauth2_client_credentials"]
assert isinstance(plugin, CredentialPlugin)

def test_plugin_name_matches_enum(self):
plugin = PLUGIN_TYPES["oauth2_client_credentials"]
assert (
plugin.name
== DefaultCredentialType.OAUTH2_CLIENT_CREDENTIALS
)

def test_plugin_backend_is_callable(self):
plugin = PLUGIN_TYPES["oauth2_client_credentials"]
assert callable(plugin.backend)


class TestOAuth2RunPlugin:
"""Verify the run_plugin dispatcher routes correctly."""

def test_run_plugin_calls_backend(self):
with mock.patch.object(
CredentialPlugin, "backend"
) as mock_backend:
mock_backend.return_value = "token-value"
result = run_plugin(
"oauth2_client_credentials", {}, {}
)
assert result == "token-value"

@responses.activate
def test_run_plugin_real_backend_success(self):
responses.post(
TOKEN_URL,
json={
"access_token": FAKE_TOKEN,
"token_type": "Bearer",
"expires_in": 3600,
},
status=200,
)

result = run_plugin(
"oauth2_client_credentials",
{
"token_url": TOKEN_URL,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
{"scope": ""},
)

assert result == FAKE_TOKEN

@responses.activate
def test_run_plugin_real_backend_with_scope(self):
ado_scope = (
"499b84ac-1321-427f-aa17-267ca6975798/.default"
)
responses.post(
TOKEN_URL,
json={
"access_token": FAKE_TOKEN,
"token_type": "Bearer",
"expires_in": 3600,
},
status=200,
)

result = run_plugin(
"oauth2_client_credentials",
{
"token_url": TOKEN_URL,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
{"scope": ado_scope},
)

assert result == FAKE_TOKEN
request_body = responses.calls[0].request.body
assert "499b84ac" in request_body

@responses.activate
def test_run_plugin_real_backend_failure(self):
responses.post(
TOKEN_URL,
json={
"error": "invalid_client",
"error_description": "Bad credentials.",
},
status=401,
)

from aap_eda.core.exceptions import CredentialPluginError

with pytest.raises(CredentialPluginError):
run_plugin(
"oauth2_client_credentials",
{
"token_url": TOKEN_URL,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
{},
)
Loading