fix(models): review feedback — separator vocabulary, constraints stub, import style

The scopes sanitize now shares the registry guard's separator
vocabulary: tab/newline/CR read as spaces, and every other C0 byte —
including the U+001C–U+001F block str.split() would silently promote to
separators — strips like the control it is, so a control byte inside a
token can never split it into two valid-looking scopes (pinned
alongside the registry's refusal).

The livepass auth-constraints stub serves the new
app_identity_auth_modes field so the pass exercises the served-data
path for the model list's auth badge, and the session-module import in
the mint tests drops to the string-path monkeypatch spelling
(single-style imports).
This commit is contained in:
Patrick Buckley
2026-08-04 05:05:26 -07:00
parent 8605c9783d
commit 1d7db73305
4 changed files with 20 additions and 14 deletions
+1
View File
@@ -501,6 +501,7 @@ CONSOLE_TEMPLATE = """<!doctype html>
auth_grant_profile: "entra",
dynamic_auth_modes: ["entra_app", "entra_obo", "rfc8693_obo"],
scopes_auth_modes: ["rfc8693_obo"],
app_identity_auth_modes: ["entra_app"],
auth_mode_profiles: {
entra_app: "entra", entra_obo: "entra",
rfc8693_obo: "rfc8693",
+1 -3
View File
@@ -1640,9 +1640,7 @@ class TestModelOboToken:
"""A delegated mode with no registered grant-profile pairing cannot
pin a leg, so the dispatch refuses loudly before the mint bridge —
minting with leg=None would run the pre-dedicated-mode overload."""
import turnstone.core.session as session_module
monkeypatch.setattr(session_module, "MODEL_AUTH_MODE_PROFILES", {})
monkeypatch.setattr("turnstone.core.session.MODEL_AUTH_MODE_PROFILES", {})
reg = _registry_with(self._obo_cfg())
sess = _fake_session(registry=reg, user_id=USER, mint_token="never")
with pytest.raises(BackendAuthUnavailableError, match="grant-profile pairing"):
+6 -3
View File
@@ -2982,14 +2982,17 @@ def test_obo_scopes_normalizers_agree_across_modules() -> None:
assert mr_module.sanitize_backend_auth_scopes(dirty) == "aud-gwopenid"
# The C0 separator block counts as Python whitespace, so a bare
# str.split() would swallow it before the guard could refuse; the
# registry must refuse it like every other control byte, while the
# sanctioned separators (tab/newline/CR, blessed in the corpus above)
# keep collapsing.
# registry must refuse it like every other control byte, and the
# SANITIZE must strip it like every other control — never promote it to
# a separator that splits one token into two valid-looking scopes —
# while the sanctioned separators (tab/newline/CR, blessed in the
# corpus above) keep collapsing.
for sep_byte in (chr(0x1C), chr(0x1D), chr(0x1E), chr(0x1F)):
with pytest.raises(mr_module.ModelAuthConfigError):
mr_module._normalize_auth_mode(
"gw", "rfc8693_obo", "api://gw", f"aud-gw{sep_byte}openid"
)
assert mr_module.sanitize_backend_auth_scopes(f"aud-gw{sep_byte}openid") == "aud-gwopenid"
def test_control_bearing_alias_refuses_to_load() -> None:
+12 -8
View File
@@ -269,18 +269,22 @@ def strip_control_characters(value: str) -> str:
def sanitize_backend_auth_scopes(value: Any) -> str:
"""The ONE spelling of the backend-auth scopes sanitize.
Collapse whitespace runs to single spaces, strip the remaining C0/DEL
control characters, then re-collapse the runs the stripping can reopen
(``"a \\x01 b"`` becomes ``"a b"`` becomes ``"a b"``). Collapse-first
ordering is load-bearing: stripping a tab-separated list first would
CONCATENATE the scopes the tab separates. No length cap and no refusal —
policy (caps, and refuse-vs-strip on garbage) stays with each consuming
The sanctioned separators — tab, newline, CR — read as spaces FIRST
(stripping a tab-separated list outright would CONCATENATE the scopes
the tab separates), then every remaining C0/DEL control character is
stripped, and finally whitespace runs collapse to single spaces. The
separator vocabulary deliberately matches the registry guard's: the C0
separator block (U+001CU+001F) is a CONTROL here, never a separator —
a bare ``str.split()`` would silently promote it to one — so a control
byte inside a token strips-and-joins rather than splitting the token
into two valid-looking scopes. No length cap and no refusal — policy
(caps, and refuse-vs-strip on garbage) stays with each consuming
layer; this function only fixes the shared spelling those policies
measure, so the console store, the registry load, and the mint request
can never disagree on what a scopes value *is*.
"""
collapsed = " ".join(str(value or "").split())
return " ".join(strip_control_characters(collapsed).split())
blessed = re.sub(r"[\t\n\r]", " ", str(value or ""))
return " ".join(strip_control_characters(blessed).split())
def _check_auth_text(alias: str, field: str, value: str) -> None: