Skip to content

Commit 756ad51

Browse files
Fix: add auth headers and body to multitenancy token request (#908)
* fix: add auth headers and body to multitenancy token request Signed-off-by: Yuki I <omoge.real@gmail.com> * fix: address PR review (redundant try/except, mocks, cleanup tests) Signed-off-by: Yuki I <omoge.real@gmail.com> --------- Signed-off-by: Yuki I <omoge.real@gmail.com> Co-authored-by: Yuki I <omoge.real@gmail.com> Co-authored-by: Emiliano Suñé <emiliano.sune@gmail.com>
1 parent 79c45f4 commit 756ad51

File tree

2 files changed

+184
-29
lines changed

2 files changed

+184
-29
lines changed

oidc-controller/api/core/acapy/config.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,44 @@ class MultiTenantAcapy:
2121
@cache
2222
def get_wallet_token(self):
2323
logger.debug(">>> get_wallet_token")
24+
25+
# Check if admin API key is configured
26+
admin_api_key_configured = (
27+
settings.ST_ACAPY_ADMIN_API_KEY_NAME and settings.ST_ACAPY_ADMIN_API_KEY
28+
)
29+
30+
headers = {}
31+
32+
if admin_api_key_configured:
33+
logger.debug("Admin API key is configured, adding to request headers")
34+
headers[settings.ST_ACAPY_ADMIN_API_KEY_NAME] = (
35+
settings.ST_ACAPY_ADMIN_API_KEY
36+
)
37+
else:
38+
logger.debug(
39+
"No admin API key configured, proceeding without authentication headers"
40+
)
41+
42+
payload = {"wallet_key": self.wallet_key}
43+
2444
resp_raw = requests.post(
2545
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{self.wallet_id}/token",
46+
headers=headers,
47+
json=payload,
2648
)
27-
assert (
28-
resp_raw.status_code == 200
29-
), f"{resp_raw.status_code}::{resp_raw.content}"
49+
50+
if resp_raw.status_code != 200:
51+
error_detail = resp_raw.content.decode()
52+
logger.error(
53+
f"Failed to get wallet token. Status: {resp_raw.status_code}, Detail: {error_detail}"
54+
)
55+
# Raising Exception to be caught by the except block below or propagated
56+
raise Exception(f"{resp_raw.status_code}::{error_detail}")
57+
3058
resp = json.loads(resp_raw.content)
3159
wallet_token = resp["token"]
32-
logger.debug("<<< get_wallet_token")
3360

61+
logger.debug("<<< get_wallet_token")
3462
return wallet_token
3563

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

4068
class SingleTenantAcapy:
4169
def get_headers(self) -> dict[str, str]:
42-
return {settings.ST_ACAPY_ADMIN_API_KEY_NAME: settings.ST_ACAPY_ADMIN_API_KEY}
70+
# Check if admin API key is configured
71+
admin_api_key_configured = (
72+
settings.ST_ACAPY_ADMIN_API_KEY_NAME and settings.ST_ACAPY_ADMIN_API_KEY
73+
)
74+
75+
if admin_api_key_configured:
76+
return {
77+
settings.ST_ACAPY_ADMIN_API_KEY_NAME: settings.ST_ACAPY_ADMIN_API_KEY
78+
}
79+
else:
80+
logger.debug("No admin API key configured for single tenant agent")
81+
return {}

oidc-controller/api/core/acapy/tests/test_config.py

Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@
77
@pytest.mark.asyncio
88
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "name")
99
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY", "key")
10-
async def test_single_tenant_has_expected_headers():
10+
async def test_single_tenant_has_expected_headers_configured():
1111
acapy = SingleTenantAcapy()
1212
headers = acapy.get_headers()
1313
assert headers == {"name": "key"}
1414

1515

16+
@pytest.mark.asyncio
17+
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "name")
18+
@mock.patch.object(settings, "ST_ACAPY_ADMIN_API_KEY", None)
19+
async def test_single_tenant_empty_headers_not_configured():
20+
# Test behavior when API key is missing
21+
acapy = SingleTenantAcapy()
22+
headers = acapy.get_headers()
23+
assert headers == {}
24+
25+
1626
@pytest.mark.asyncio
1727
async def test_multi_tenant_get_headers_returns_bearer_token_auth(requests_mock):
1828
acapy = MultiTenantAcapy()
@@ -23,29 +33,135 @@ async def test_multi_tenant_get_headers_returns_bearer_token_auth(requests_mock)
2333

2434
@pytest.mark.asyncio
2535
async def test_multi_tenant_get_wallet_token_returns_token_at_token_key(requests_mock):
26-
requests_mock.post(
27-
settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token",
28-
headers={},
29-
json={"token": "token"},
30-
status_code=200,
31-
)
32-
acapy = MultiTenantAcapy()
33-
acapy.wallet_id = "wallet_id"
34-
token = acapy.get_wallet_token()
35-
assert token == "token"
36+
wallet_id = "wallet_id"
37+
wallet_key = "wallet_key"
38+
39+
with mock.patch.object(
40+
settings, "MT_ACAPY_WALLET_ID", wallet_id
41+
), mock.patch.object(settings, "MT_ACAPY_WALLET_KEY", wallet_key):
42+
43+
requests_mock.post(
44+
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
45+
headers={},
46+
json={"token": "token"},
47+
status_code=200,
48+
)
49+
50+
acapy = MultiTenantAcapy()
51+
acapy.wallet_id = wallet_id
52+
acapy.wallet_key = wallet_key
53+
acapy.get_wallet_token.cache_clear()
54+
55+
token = acapy.get_wallet_token()
56+
assert token == "token"
3657

3758

3859
@pytest.mark.asyncio
39-
async def test_multi_tenant_throws_assertion_error_for_non_200_response(requests_mock):
40-
requests_mock.post(
41-
settings.ACAPY_ADMIN_URL + "/multitenancy/wallet/wallet_id/token",
42-
headers={},
43-
json={"token": "token"},
44-
status_code=400,
45-
)
46-
acapy = MultiTenantAcapy()
47-
acapy.wallet_id = "wallet_id"
48-
try:
49-
acapy.get_wallet_token()
50-
except AssertionError as e:
51-
assert e is not None
60+
async def test_multi_tenant_get_wallet_token_includes_auth_headers_and_body(
61+
requests_mock,
62+
):
63+
# Verify headers and body payload
64+
wallet_id = "wallet_id"
65+
wallet_key = "wallet_key"
66+
admin_key = "admin_key"
67+
admin_header = "x-api-key"
68+
69+
# Mock settings for the duration of this test
70+
with mock.patch.object(
71+
settings, "MT_ACAPY_WALLET_ID", wallet_id
72+
), mock.patch.object(
73+
settings, "MT_ACAPY_WALLET_KEY", wallet_key
74+
), mock.patch.object(
75+
settings, "ST_ACAPY_ADMIN_API_KEY", admin_key
76+
), mock.patch.object(
77+
settings, "ST_ACAPY_ADMIN_API_KEY_NAME", admin_header
78+
):
79+
80+
acapy = MultiTenantAcapy()
81+
# Ensure we use the values we expect (class init reads settings once)
82+
acapy.wallet_id = wallet_id
83+
acapy.wallet_key = wallet_key
84+
# Ensure we bypass cache from any previous tests
85+
acapy.get_wallet_token.cache_clear()
86+
87+
requests_mock.post(
88+
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
89+
json={"token": "token"},
90+
status_code=200,
91+
)
92+
93+
token = acapy.get_wallet_token()
94+
assert token == "token"
95+
96+
# Verify request details
97+
last_request = requests_mock.last_request
98+
assert last_request.headers[admin_header] == admin_key
99+
assert last_request.json() == {"wallet_key": wallet_key}
100+
101+
102+
@pytest.mark.asyncio
103+
async def test_multi_tenant_get_wallet_token_no_auth_headers_when_not_configured(
104+
requests_mock,
105+
):
106+
# Test insecure mode behavior
107+
wallet_id = "wallet_id"
108+
wallet_key = "wallet_key"
109+
110+
# Mock settings with None for admin key
111+
with mock.patch.object(
112+
settings, "MT_ACAPY_WALLET_ID", wallet_id
113+
), mock.patch.object(
114+
settings, "MT_ACAPY_WALLET_KEY", wallet_key
115+
), mock.patch.object(
116+
settings, "ST_ACAPY_ADMIN_API_KEY", None
117+
), mock.patch.object(
118+
settings, "ST_ACAPY_ADMIN_API_KEY_NAME", "x-api-key"
119+
):
120+
121+
acapy = MultiTenantAcapy()
122+
acapy.wallet_id = wallet_id
123+
acapy.wallet_key = wallet_key
124+
acapy.get_wallet_token.cache_clear()
125+
126+
requests_mock.post(
127+
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
128+
json={"token": "token"},
129+
status_code=200,
130+
)
131+
132+
token = acapy.get_wallet_token()
133+
assert token == "token"
134+
135+
# Verify request details
136+
last_request = requests_mock.last_request
137+
# Headers might contain Content-Type, but should not contain the api key
138+
assert "x-api-key" not in last_request.headers
139+
assert last_request.json() == {"wallet_key": wallet_key}
140+
141+
142+
@pytest.mark.asyncio
143+
async def test_multi_tenant_throws_exception_for_401_unauthorized(requests_mock):
144+
wallet_id = "wallet_id"
145+
wallet_key = "wallet_key"
146+
147+
with mock.patch.object(
148+
settings, "MT_ACAPY_WALLET_ID", wallet_id
149+
), mock.patch.object(settings, "MT_ACAPY_WALLET_KEY", wallet_key):
150+
151+
acapy = MultiTenantAcapy()
152+
acapy.wallet_id = wallet_id
153+
acapy.wallet_key = wallet_key
154+
acapy.get_wallet_token.cache_clear()
155+
156+
requests_mock.post(
157+
settings.ACAPY_ADMIN_URL + f"/multitenancy/wallet/{wallet_id}/token",
158+
json={"error": "unauthorized"},
159+
status_code=401,
160+
)
161+
162+
# Check for generic Exception, as the code now raises Exception(f"{code}::{detail}")
163+
with pytest.raises(Exception) as excinfo:
164+
acapy.get_wallet_token()
165+
166+
assert "401" in str(excinfo.value)
167+
assert "unauthorized" in str(excinfo.value)

0 commit comments

Comments
 (0)