Skip to content

Commit fe9b7d3

Browse files
committed
refactor(server): isolate Orbit identity URL selection
1 parent 7680e10 commit fe9b7d3

4 files changed

Lines changed: 73 additions & 26 deletions

File tree

‎server/src/agent_control_server/auth_framework/config.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import os
3333
import ssl
3434
from dataclasses import dataclass
35+
from urllib.parse import urlsplit, urlunsplit
3536

3637
from ..config import auth_settings
3738
from ..logging_utils import get_logger
@@ -64,6 +65,8 @@
6465
_UPSTREAM_MAX_KEEPALIVE_CONNECTIONS_ENV = (
6566
"AGENT_CONTROL_AUTH_UPSTREAM_MAX_KEEPALIVE_CONNECTIONS"
6667
)
68+
_ORBIT_MANAGEMENT_PATH = "/internal/auth/agent_control/check_management_access"
69+
_ORBIT_IDENTITY_PATH = "/internal/auth/resolve_tenant_context"
6770

6871
# Runtime flow.
6972
_RUNTIME_MODE_ENV = "AGENT_CONTROL_RUNTIME_AUTH_MODE"
@@ -246,10 +249,14 @@ def _build_default_provider() -> RequestAuthorizer:
246249
max_keepalive_connections=max_keepalive_connections,
247250
)
248251
_logger.info("Default auth provider: http_upstream url=%s", url)
252+
# Only the known Orbit management route has a predictable identity route.
253+
# Other upstreams keep their existing namespace-wide authorization flow.
254+
explicit_identity_url = (os.environ.get(_UPSTREAM_IDENTITY_URL_ENV) or "").strip()
255+
identity_url = explicit_identity_url or _derive_orbit_identity_url(url)
249256
try:
250257
upstream_config = HttpUpstreamConfig(
251258
url=url,
252-
identity_url=(os.environ.get(_UPSTREAM_IDENTITY_URL_ENV) or "").strip() or None,
259+
identity_url=identity_url,
253260
timeout_seconds=timeout,
254261
service_token=token,
255262
service_token_header=token_header,
@@ -277,6 +284,17 @@ def _build_default_provider() -> RequestAuthorizer:
277284
)
278285

279286

287+
def _derive_orbit_identity_url(url: str) -> str | None:
288+
"""Resolve Orbit's existing identity route from its management route."""
289+
parts = urlsplit(url)
290+
if not parts.path.endswith(_ORBIT_MANAGEMENT_PATH):
291+
return None
292+
prefix = parts.path[: -len(_ORBIT_MANAGEMENT_PATH)]
293+
return urlunsplit(
294+
(parts.scheme, parts.netloc, prefix + _ORBIT_IDENTITY_PATH, parts.query, parts.fragment)
295+
)
296+
297+
280298
def _validate_local_api_key_mode(mode_env: str = _MODE_ENV) -> None:
281299
"""Fail startup when local API-key mode has no local key validator."""
282300
if not auth_settings.api_key_enabled:

‎server/src/agent_control_server/auth_framework/providers/http_upstream.py‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
from datetime import datetime
4747
from time import perf_counter
4848
from typing import Any
49-
from urllib.parse import urlsplit, urlunsplit
5049

5150
import httpx
5251
from agent_control_models.errors import ErrorCode, ErrorReason
@@ -68,9 +67,8 @@
6867
_logger = get_logger(__name__)
6968

7069
_DEFAULT_FORWARDED_HEADERS = ("X-API-Key", "Authorization", "Cookie")
71-
_ORBIT_MANAGEMENT_PATH = "/internal/auth/agent_control/check_management_access"
72-
_ORBIT_IDENTITY_PATH = "/internal/auth/resolve_tenant_context"
73-
_IDENTITY_OPERATION = "identity.resolve"
70+
# Diagnostic label only; the identity POST has no operation payload.
71+
_IDENTITY_LOOKUP_LABEL = "identity.resolve"
7472

7573
_AUTH_UPSTREAM_ATTEMPTS = Counter(
7674
"agent_control_server_auth_upstream_attempts_total",
@@ -185,8 +183,7 @@ class HttpUpstreamConfig:
185183
"""Maximum idle connections retained for the auth upstream."""
186184

187185
identity_url: str | None = None
188-
"""URL for credential and namespace resolution. If omitted, the Orbit
189-
identity URL is derived from the known management authorization path."""
186+
"""Optional URL for credential and namespace resolution before target authorization."""
190187

191188
def __post_init__(self) -> None:
192189
if self.keepalive_expiry_seconds < 0:
@@ -221,7 +218,7 @@ def __init__(
221218
client: httpx.AsyncClient | None = None,
222219
) -> None:
223220
self._config = config
224-
self._identity_url = config.identity_url or _derive_orbit_identity_url(config.url)
221+
self._identity_url = config.identity_url
225222
self._owns_client = client is None
226223
if client is not None:
227224
self._client = client
@@ -274,7 +271,7 @@ async def resolve_identity(self, request: Request, operation: Operation) -> Prin
274271
hint="Set AGENT_CONTROL_AUTH_UPSTREAM_IDENTITY_URL.",
275272
)
276273
response = await self._post_upstream(
277-
_IDENTITY_OPERATION,
274+
_IDENTITY_LOOKUP_LABEL,
278275
None,
279276
self._forward_headers(request),
280277
url=self._identity_url,
@@ -287,7 +284,7 @@ async def resolve_identity(self, request: Request, operation: Operation) -> Prin
287284
detail="Authorization identity endpoint was not found.",
288285
hint="Check the configured authorization identity URL.",
289286
)
290-
principal = self._handle_response(response, _IDENTITY_OPERATION, None)
287+
principal = self._handle_response(response, _IDENTITY_LOOKUP_LABEL, None)
291288
return Principal(namespace_key=principal.namespace_key, caller_id=principal.caller_id)
292289

293290
async def _post_upstream(
@@ -493,17 +490,6 @@ def _authorization_service_unavailable_error() -> APIError:
493490
)
494491

495492

496-
def _derive_orbit_identity_url(url: str) -> str | None:
497-
"""Resolve Orbit's existing identity route from its management route."""
498-
parts = urlsplit(url)
499-
if not parts.path.endswith(_ORBIT_MANAGEMENT_PATH):
500-
return None
501-
prefix = parts.path[: -len(_ORBIT_MANAGEMENT_PATH)]
502-
return urlunsplit(
503-
(parts.scheme, parts.netloc, prefix + _ORBIT_IDENTITY_PATH, parts.query, parts.fragment)
504-
)
505-
506-
507493
def _ensure_target_context_matches_grant(
508494
context: dict[str, Any] | None,
509495
principal: Principal,

‎server/tests/test_auth_framework.py‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def factory(request: httpx.Request) -> httpx.Response:
280280

281281

282282
@pytest.mark.asyncio
283-
async def test_http_upstream_resolves_identity_at_derived_orbit_url():
283+
async def test_http_upstream_resolves_identity_at_configured_url():
284284
captured: dict[str, Any] = {}
285285

286286
def factory(request: httpx.Request) -> httpx.Response:
@@ -300,7 +300,10 @@ def factory(request: httpx.Request) -> httpx.Response:
300300
provider = _build_upstream(
301301
factory,
302302
config_overrides={
303-
"url": ("https://orbit.example/prefix/internal/auth/agent_control/check_management_access"),
303+
"url": (
304+
"https://orbit.example/prefix/internal/auth/agent_control/check_management_access"
305+
),
306+
"identity_url": "https://orbit.example/prefix/internal/auth/resolve_tenant_context",
304307
"service_token": "service-secret",
305308
"extra_forward_headers": ("X-Deployer-Auth",),
306309
},
@@ -1671,13 +1674,51 @@ async def test_configure_http_upstream_extra_forward_headers_env(monkeypatch):
16711674
await auth_config.teardown_auth()
16721675

16731676

1677+
@pytest.mark.parametrize(
1678+
("upstream_url", "expected_identity_url"),
1679+
[
1680+
(
1681+
"https://orbit.example/prefix/internal/auth/agent_control/check_management_access",
1682+
"https://orbit.example/prefix/internal/auth/resolve_tenant_context",
1683+
),
1684+
("https://auth.example.test/check", None),
1685+
],
1686+
)
16741687
@pytest.mark.asyncio
1675-
async def test_configure_http_upstream_identity_url_env(monkeypatch):
1688+
async def test_configure_http_upstream_selects_identity_url(
1689+
monkeypatch, upstream_url: str, expected_identity_url: str | None
1690+
):
16761691
from agent_control_server.auth_framework import config as auth_config
16771692

16781693
clear_authorizers()
16791694
monkeypatch.setenv("AGENT_CONTROL_AUTH_MODE", "http_upstream")
1680-
monkeypatch.setenv("AGENT_CONTROL_AUTH_UPSTREAM_URL", "https://auth.example.test/check")
1695+
monkeypatch.setenv("AGENT_CONTROL_AUTH_UPSTREAM_URL", upstream_url)
1696+
monkeypatch.delenv("AGENT_CONTROL_AUTH_UPSTREAM_IDENTITY_URL", raising=False)
1697+
1698+
try:
1699+
auth_config.configure_auth_from_env()
1700+
provider = get_authorizer(Operation.CONTROL_BINDINGS_READ)
1701+
assert isinstance(provider, HttpUpstreamAuthProvider)
1702+
assert provider._config.identity_url == expected_identity_url
1703+
assert provider.binding_target_authorization is (expected_identity_url is not None)
1704+
finally:
1705+
await auth_config.teardown_auth()
1706+
1707+
1708+
@pytest.mark.parametrize(
1709+
"upstream_url",
1710+
[
1711+
"https://auth.example.test/check",
1712+
"https://orbit.example/internal/auth/agent_control/check_management_access",
1713+
],
1714+
)
1715+
@pytest.mark.asyncio
1716+
async def test_configure_http_upstream_identity_url_env(monkeypatch, upstream_url: str):
1717+
from agent_control_server.auth_framework import config as auth_config
1718+
1719+
clear_authorizers()
1720+
monkeypatch.setenv("AGENT_CONTROL_AUTH_MODE", "http_upstream")
1721+
monkeypatch.setenv("AGENT_CONTROL_AUTH_UPSTREAM_URL", upstream_url)
16811722
monkeypatch.setenv(
16821723
"AGENT_CONTROL_AUTH_UPSTREAM_IDENTITY_URL",
16831724
" https://identity.example.test/resolve ",
@@ -1688,6 +1729,7 @@ async def test_configure_http_upstream_identity_url_env(monkeypatch):
16881729
provider = get_authorizer(Operation.CONTROL_BINDINGS_READ)
16891730
assert isinstance(provider, HttpUpstreamAuthProvider)
16901731
assert provider._config.identity_url == "https://identity.example.test/resolve"
1732+
assert provider.binding_target_authorization is True
16911733
finally:
16921734
await auth_config.teardown_auth()
16931735

‎server/tests/test_control_bindings_endpoints.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,8 @@ def upstream_auth(request: httpx.Request) -> Response:
473473
set_authorizer(
474474
HttpUpstreamAuthProvider(
475475
HttpUpstreamConfig(
476-
url="https://orbit.example/internal/auth/agent_control/check_management_access"
476+
url="https://orbit.example/internal/auth/agent_control/check_management_access",
477+
identity_url="https://orbit.example/internal/auth/resolve_tenant_context",
477478
),
478479
client=upstream_client,
479480
)

0 commit comments

Comments
 (0)