Skip to content
Merged
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
49 changes: 44 additions & 5 deletions oidc-controller/api/core/acapy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,44 @@ class MultiTenantAcapy:
@cache
def get_wallet_token(self):
logger.debug(">>> get_wallet_token")

# Check if admin API key is configured
admin_api_key_configured = (
settings.ST_ACAPY_ADMIN_API_KEY_NAME and settings.ST_ACAPY_ADMIN_API_KEY
)

headers = {}

if admin_api_key_configured:
logger.debug("Admin API key is configured, adding to request headers")
headers[settings.ST_ACAPY_ADMIN_API_KEY_NAME] = (
settings.ST_ACAPY_ADMIN_API_KEY
)
else:
logger.debug(
"No admin API key configured, proceeding without authentication headers"
)

payload = {"wallet_key": self.wallet_key}

resp_raw = requests.post(
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{self.wallet_id}/token",
headers=headers,
json=payload,
)
assert (
resp_raw.status_code == 200
), f"{resp_raw.status_code}::{resp_raw.content}"

if resp_raw.status_code != 200:
error_detail = resp_raw.content.decode()
logger.error(
f"Failed to get wallet token. Status: {resp_raw.status_code}, Detail: {error_detail}"
)
# Raising Exception to be caught by the except block below or propagated
raise Exception(f"{resp_raw.status_code}::{error_detail}")

resp = json.loads(resp_raw.content)
wallet_token = resp["token"]
logger.debug("<<< get_wallet_token")

logger.debug("<<< get_wallet_token")
return wallet_token

def get_headers(self) -> dict[str, str]:
Expand All @@ -39,4 +67,15 @@ def get_headers(self) -> dict[str, str]:

class SingleTenantAcapy:
def get_headers(self) -> dict[str, str]:
return {settings.ST_ACAPY_ADMIN_API_KEY_NAME: settings.ST_ACAPY_ADMIN_API_KEY}
# Check if admin API key is configured
admin_api_key_configured = (
settings.ST_ACAPY_ADMIN_API_KEY_NAME and settings.ST_ACAPY_ADMIN_API_KEY
)

if admin_api_key_configured:
return {
settings.ST_ACAPY_ADMIN_API_KEY_NAME: settings.ST_ACAPY_ADMIN_API_KEY
}
else:
logger.debug("No admin API key configured for single tenant agent")
return {}
164 changes: 140 additions & 24 deletions oidc-controller/api/core/acapy/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@
@pytest.mark.asyncio
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "name")
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY", "key")
async def test_single_tenant_has_expected_headers():
async def test_single_tenant_has_expected_headers_configured():
acapy = SingleTenantAcapy()
headers = acapy.get_headers()
assert headers == {"name": "key"}


@pytest.mark.asyncio
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "name")
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY", None)
async def test_single_tenant_empty_headers_not_configured():
# Test behavior when API key is missing
acapy = SingleTenantAcapy()
headers = acapy.get_headers()
assert headers == {}


@pytest.mark.asyncio
async def test_multi_tenant_get_headers_returns_bearer_token_auth(requests_mock):
acapy = MultiTenantAcapy()
Expand All @@ -23,29 +33,135 @@ async def test_multi_tenant_get_headers_returns_bearer_token_auth(requests_mock)

@pytest.mark.asyncio
async def test_multi_tenant_get_wallet_token_returns_token_at_token_key(requests_mock):
requests_mock.post(
settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token",
headers={},
json={"token": "token"},
status_code=200,
)
acapy = MultiTenantAcapy()
acapy.wallet_id = "wallet_id"
token = acapy.get_wallet_token()
assert token == "token"
wallet_id = "wallet_id"
wallet_key = "wallet_key"

with mock.patch.object(
settings, "MT_ACAPY_WALLET_ID", wallet_id
), mock.patch.object(settings, "MT_ACAPY_WALLET_KEY", wallet_key):

requests_mock.post(
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
headers={},
json={"token": "token"},
status_code=200,
)

acapy = MultiTenantAcapy()
acapy.wallet_id = wallet_id
acapy.wallet_key = wallet_key
acapy.get_wallet_token.cache_clear()

token = acapy.get_wallet_token()
assert token == "token"


@pytest.mark.asyncio
async def test_multi_tenant_throws_assertion_error_for_non_200_response(requests_mock):
requests_mock.post(
settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token",
headers={},
json={"token": "token"},
status_code=400,
)
acapy = MultiTenantAcapy()
acapy.wallet_id = "wallet_id"
try:
acapy.get_wallet_token()
except AssertionError as e:
assert e is not None
async def test_multi_tenant_get_wallet_token_includes_auth_headers_and_body(
requests_mock,
):
# Verify headers and body payload
wallet_id = "wallet_id"
wallet_key = "wallet_key"
admin_key = "admin_key"
admin_header = "x-api-key"

# Mock settings for the duration of this test
with mock.patch.object(
settings, "MT_ACAPY_WALLET_ID", wallet_id
), mock.patch.object(
settings, "MT_ACAPY_WALLET_KEY", wallet_key
), mock.patch.object(
settings, "ST_ACAPY_ADMIN_API_KEY", admin_key
), mock.patch.object(
settings, "ST_ACAPY_ADMIN_API_KEY_NAME", admin_header
):

acapy = MultiTenantAcapy()
# Ensure we use the values we expect (class init reads settings once)
acapy.wallet_id = wallet_id
acapy.wallet_key = wallet_key
# Ensure we bypass cache from any previous tests
acapy.get_wallet_token.cache_clear()

requests_mock.post(
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
json={"token": "token"},
status_code=200,
)

token = acapy.get_wallet_token()
assert token == "token"

# Verify request details
last_request = requests_mock.last_request
assert last_request.headers[admin_header] == admin_key
assert last_request.json() == {"wallet_key": wallet_key}


@pytest.mark.asyncio
async def test_multi_tenant_get_wallet_token_no_auth_headers_when_not_configured(
requests_mock,
):
# Test insecure mode behavior
wallet_id = "wallet_id"
wallet_key = "wallet_key"

# Mock settings with None for admin key
with mock.patch.object(
settings, "MT_ACAPY_WALLET_ID", wallet_id
), mock.patch.object(
settings, "MT_ACAPY_WALLET_KEY", wallet_key
), mock.patch.object(
settings, "ST_ACAPY_ADMIN_API_KEY", None
), mock.patch.object(
settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "x-api-key"
):

acapy = MultiTenantAcapy()
acapy.wallet_id = wallet_id
acapy.wallet_key = wallet_key
acapy.get_wallet_token.cache_clear()

requests_mock.post(
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
json={"token": "token"},
status_code=200,
)

token = acapy.get_wallet_token()
assert token == "token"

# Verify request details
last_request = requests_mock.last_request
# Headers might contain Content-Type, but should not contain the api key
assert "x-api-key" not in last_request.headers
assert last_request.json() == {"wallet_key": wallet_key}


@pytest.mark.asyncio
async def test_multi_tenant_throws_exception_for_401_unauthorized(requests_mock):
wallet_id = "wallet_id"
wallet_key = "wallet_key"

with mock.patch.object(
settings, "MT_ACAPY_WALLET_ID", wallet_id
), mock.patch.object(settings, "MT_ACAPY_WALLET_KEY", wallet_key):

acapy = MultiTenantAcapy()
acapy.wallet_id = wallet_id
acapy.wallet_key = wallet_key
acapy.get_wallet_token.cache_clear()

requests_mock.post(
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
json={"error": "unauthorized"},
status_code=401,
)

# Check for generic Exception, as the code now raises Exception(f"{code}::{detail}")
with pytest.raises(Exception) as excinfo:
acapy.get_wallet_token()

assert "401" in str(excinfo.value)
assert "unauthorized" in str(excinfo.value)