mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
33ace975d2
Follow-up to the per-alias Entra OBO/app-identity backend auth: the console write path now applies default-deny field classification, the admin shelf gains full backend-auth support, and the session/registry rebind machinery is hardened for config changes landing under live sessions. Console write gate: - Default-deny classification: any non-neutral change to a row that is or becomes dynamic requires admin.mcp plus validation; the provably auth-neutral columns are enumerated (MODEL_AUTH_NEUTRAL_FIELDS) and a live-schema classification test forces every future column to be classified. The derivation is a pure function (_derive_auth_gate) with unit-pinned exclusivity invariants. - Two-tier validation mirroring the MCP oauth_obo validator: the row tier (audience allow-list) runs on every gated write; the posture tier (OIDC configured, token store present) runs on pair changes and on enable-arming. - Pure-disable carve-out: disabling a dynamic row is de-escalation and is never blocked — admin.models suffices and validation is skipped, including for rows with corrupt or skewed stored values. - Capabilities are compared canonically (key order, integral floats), the audience compare normalizes both sides, and staging an audience on a static row is refused on both write twins. - Calibrate writes the capabilities column under an enforced confinement invariant with a compare-and-swap persist. Admin shelf: - Backend-auth section with a per-open constraints fetch (GET /model-definitions/auth-constraints: audience allow-list, grant profile, dynamic modes), datalist audience suggestions, server-defined modes preserved on round-trip, and permission-aware visibility built on cache-skew-safe helpers shared through auth.js. - Refused live-registry swaps surface as an amber registry_warning on the write, delete, reload, and calibrate responses; audit rows carry auth_gated / auth_disarmed markers visible in the audit view. Registry and sessions: - The encryption-key requirement for dynamic auth is enforced inside ModelRegistry.reload() itself — nodes refuse with 503 and the console records coord_registry_error — and reload bumps the generation before the map swap so a racing reader can never pair a stale generation with new maps. - resolve()/resolve_binding() return the generation from inside the registry lock; sessions rebind per send on generation change with atomic client/provider/config commits, fallback-first handling of removed or unconstructable aliases, and judge/limiter resets only when the binding actually changed. - Mint refusals record per-user causes surfaced in the per-turn heartbeat logs; misconfiguration warnings are deduplicated with bounded state. Verification: 10417 tests (99 added on this branch), a 71-scenario browser harness over the real admin shelf, and a live rfc8693 token-exchange e2e run (MCP legs verified end to end; the model-leg scope gap is tracked as #955 under a narrow known-gap signature). Closes #950.
58 lines
2.0 KiB
Python
58 lines
2.0 KiB
Python
"""Shared OIDC posture builder for the model-auth / OBO test surface.
|
|
|
|
One construction site for the posture the mint and write-validator suites
|
|
read, built as a REAL (frozen) ``OIDCConfig`` so an override for a field
|
|
the dataclass does not carry raises at the call site. Named with a leading
|
|
underscore so pytest does not collect it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from turnstone.core.oidc import OIDCConfig
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
# The issuer / token-endpoint pair the mint suites route their mock
|
|
# transports on.
|
|
ISSUER = "https://idp.test"
|
|
TOKEN_ENDPOINT = "https://idp.test/token"
|
|
|
|
|
|
def make_oidc_config(**overrides: Any) -> OIDCConfig:
|
|
"""A full, mintable OIDC posture; tests override the field under test,
|
|
everything else rides the dataclass defaults."""
|
|
defaults: dict[str, Any] = {
|
|
"enabled": True,
|
|
"issuer": ISSUER,
|
|
"client_id": "cid",
|
|
"client_secret": "csecret",
|
|
"token_endpoint": TOKEN_ENDPOINT,
|
|
}
|
|
defaults.update(overrides)
|
|
return OIDCConfig(**defaults)
|
|
|
|
|
|
def keyed_app_state() -> SimpleNamespace:
|
|
"""App-state stub satisfying ``ModelRegistry.reload``'s dynamic-auth key
|
|
guard, for suites exercising reload mechanics rather than key policy."""
|
|
return SimpleNamespace(mcp_token_store=object())
|
|
|
|
|
|
def mint_warn_state_reset() -> Iterator[None]:
|
|
"""Reset generator behind the mint suites' autouse fixtures: empties the
|
|
process-global mint warn/dedup/cause state before AND after each test,
|
|
so warn-dedup assertions are not order-dependent. Modules install it as
|
|
``yield from mint_warn_state_reset()`` in an autouse fixture.
|
|
"""
|
|
# Lazy import: non-mint consumers of this helper module (the write-
|
|
# validator suites) shouldn't pay the mcp_oauth import.
|
|
from turnstone.core.mcp_oauth import reset_model_mint_warn_state_for_tests
|
|
|
|
reset_model_mint_warn_state_for_tests()
|
|
yield
|
|
reset_model_mint_warn_state_for_tests()
|