feat: add OIDC browser sessions with live access enforcement
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
tegwick 2026-09-28 12:17:16 +02:00
parent fdea2f5192
commit a15fe032b0
14 changed files with 807 additions and 31 deletions

View file

@ -31,6 +31,7 @@ from hub_core.runtime.workload_projection import (
from hub_core.runtime.workload_projection_routes import create_workload_projection_router
from hub_core.security.boundary import AccessBoundary, AccessController, FactSource
from hub_core.security.config import SecuritySettings
from hub_core.security.browser import BrowserSettings, BrowserSessions, create_browser_router
def create_app(
@ -42,8 +43,11 @@ def create_app(
access_controller: AccessController | None = None,
access_facts: FactSource | None = None,
security_settings: SecuritySettings | None = None,
browser_settings: BrowserSettings | None = None,
) -> FastAPI:
resolved_settings = settings or RuntimeSettings.from_env()
if access_controller is not None and not resolved_settings.enforce_access:
raise ValueError("an access controller requires enforcement mode")
# Importing this module also constructs the standalone app. Environment
# configuration is activated only by an explicit owner-facts composition;
# without that adapter the default app stays closed, not import-broken.
@ -59,6 +63,9 @@ def create_app(
raise ValueError("security composition requires configuration and authoritative owner facts")
security_client = httpx.AsyncClient(trust_env=False)
access_controller = security_settings.compose(facts=access_facts, client=security_client)
if browser_settings is not None and (not resolved_settings.enforce_access or access_controller is None):
raise ValueError("browser sessions require an enforced access controller")
browser = BrowserSessions(settings=browser_settings, controller=access_controller) if browser_settings else None
resolved_store = port_store or _create_store(resolved_settings)
owns_store = port_store is None
resolved_repo_projection_client = repo_projection_client
@ -107,6 +114,8 @@ def create_app(
try:
yield
finally:
if browser is not None:
browser.clear()
if refresh_task is not None:
refresh_task.cancel()
with suppress(asyncio.CancelledError):
@ -135,8 +144,11 @@ def create_app(
app.state.repository_navigation = repository_navigation
app.state.workload_projection = workload_projection
app.state.access_controller = access_controller
app.state.browser_sessions = browser
if browser is not None:
app.include_router(create_browser_router())
if resolved_settings.enforce_access:
app.add_middleware(AccessBoundary, host=app, controller=access_controller)
app.add_middleware(AccessBoundary, host=app, controller=access_controller, browser=browser)
@app.get("/healthz", response_model=HealthResponse, tags=["system"])
async def healthz() -> HealthResponse: