boto3 and azure-identity freeze the credential chain into the client at construction, and the process env under a multiplexed turn belongs to the launch profile. A region-only (bedrock) / config-only (lru_cache) slot therefore signed a served profile's calls with the launch profile's keys and served its account's model list to everyone. Under a HERMES_HOME override the clients are built from the profile's secret scope (AWS_* / AZURE_* from its .env) and cached per (home, service, region) / (home, config); the discovery cache key carries the home. The unscoped path keeps the region slot and the maxsize=1 lru byte-for-byte.
91 lines
3.7 KiB
Python
91 lines
3.7 KiB
Python
"""Cloud-SDK credential clients under ``gateway.multiplex_profiles``: boto3 and azure-identity freeze the
|
|
credential chain into the client at construction, so a slot keyed by region / config alone would sign a
|
|
served profile's calls with the launch profile's keys. Each test warms the client under profile A, reads
|
|
under routed profile B whose ``.env`` differs (real temp homes, real secret scope; no mocks of the cache).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agent.secret_scope import build_profile_secret_scope, reset_secret_scope, set_secret_scope
|
|
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
|
|
|
|
|
@pytest.fixture
|
|
def two_profiles(tmp_path, monkeypatch):
|
|
a = tmp_path / ".hermes"
|
|
b = a / "profiles" / "b"
|
|
b.mkdir(parents=True)
|
|
monkeypatch.setenv("HERMES_HOME", str(a))
|
|
for var in ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_PROFILE",
|
|
"AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"):
|
|
monkeypatch.delenv(var, raising=False)
|
|
for name, home in (("A", a), ("B", b)):
|
|
(home / ".env").write_text(
|
|
f"AWS_ACCESS_KEY_ID=AKIA{name * 16}\nAWS_SECRET_ACCESS_KEY=secret-{name}\n"
|
|
f"AZURE_TENANT_ID=tenant-{name}\nAZURE_CLIENT_ID=client-{name}\nAZURE_CLIENT_SECRET=s-{name}\n",
|
|
encoding="utf-8")
|
|
return a, b
|
|
|
|
|
|
def _under(home: Path, fn):
|
|
home_token = set_hermes_home_override(str(home))
|
|
scope_token = set_secret_scope(build_profile_secret_scope(home))
|
|
try:
|
|
return fn()
|
|
finally:
|
|
reset_secret_scope(scope_token)
|
|
reset_hermes_home_override(home_token)
|
|
|
|
|
|
def test_bedrock_clients_sign_with_the_routed_profiles_aws_keys(two_profiles):
|
|
pytest.importorskip("boto3")
|
|
from agent import bedrock_adapter as ba
|
|
|
|
a, b = two_profiles
|
|
ba.reset_client_cache()
|
|
try:
|
|
client_a = _under(a, lambda: ba._get_bedrock_runtime_client("us-east-1"))
|
|
client_b = _under(b, lambda: ba._get_bedrock_runtime_client("us-east-1"))
|
|
assert client_b is not client_a
|
|
keys = {c._request_signer._credentials.get_frozen_credentials().access_key for c in (client_a, client_b)}
|
|
assert keys == {"AKIA" + "A" * 16, "AKIA" + "B" * 16}
|
|
# Per-profile slots stay hot; eviction under B leaves A's client alone.
|
|
assert _under(a, lambda: ba._get_bedrock_runtime_client("us-east-1")) is client_a
|
|
assert _under(b, lambda: ba.invalidate_runtime_client("us-east-1")) is True
|
|
assert _under(a, lambda: ba._get_bedrock_runtime_client("us-east-1")) is client_a
|
|
finally:
|
|
ba.reset_client_cache()
|
|
|
|
|
|
def test_azure_entra_credential_is_built_from_the_routed_profiles_scope(two_profiles):
|
|
from agent import azure_identity_adapter as az
|
|
|
|
a, b = two_profiles
|
|
seen: list[tuple] = []
|
|
|
|
class _FakeSDK:
|
|
def ClientSecretCredential(self, tenant, client, secret):
|
|
seen.append((tenant, client))
|
|
return object()
|
|
|
|
def DefaultAzureCredential(self, **kwargs):
|
|
seen.append(("default-chain",))
|
|
return object()
|
|
|
|
az.reset_credential_cache()
|
|
try:
|
|
cfg = az.EntraIdentityConfig()
|
|
import unittest.mock as mock
|
|
with mock.patch.object(az, "_require_azure_identity", lambda: _FakeSDK()):
|
|
cred_a = _under(a, lambda: az.build_credential(cfg))
|
|
cred_b = _under(b, lambda: az.build_credential(cfg))
|
|
assert cred_b is not cred_a
|
|
assert seen == [("tenant-A", "client-A"), ("tenant-B", "client-B")]
|
|
assert _under(a, lambda: az.build_credential(cfg)) is cred_a # cached per profile, not rebuilt
|
|
finally:
|
|
az.reset_credential_cache()
|