Skip to content

control-plane-api: no-amplification and compatibility suite - #3434

Open
bbartman wants to merge 1 commit into
bmb/3376-stack-8-unmasked-guardsfrom
bmb/3376-stack-9-no-amplification-suite
Open

control-plane-api: no-amplification and compatibility suite#3434
bbartman wants to merge 1 commit into
bmb/3376-stack-8-unmasked-guardsfrom
bmb/3376-stack-9-no-amplification-suite

Conversation

@bbartman

Copy link
Copy Markdown
Contributor

Task 7 of #3376 (PR 9 on the stack, stacked on #3427): end-to-end assertions
that a capability-masked token's authority is always a subset of its user's
live grants. Tests only — no production code changes.

What the suite is

Every assertion drives a token minted by the real capability_token
grant
through real routes, over a real snapshot of a real database — the
composed mint→use pipeline no single enforcement or mint test exercises.
Per-surface enforcement plumbing with hand-signed tokens is already pinned in
each surface's own module (3c/5/6) and is deliberately not re-proven here.

New module src/server/masked_token_suite/ (#[cfg(test)]):

  • walk — the headline property as an implication sweep: fixture
    collections × an 11-mask family, every masked row minted through the
    endpoint, asserting masked-allowed ⟹ unmasked-allowed and
    all-bundles ≡ unmasked, with the outcome grid insta-snapshotted so a
    legitimate behavior change reads as a grid diff. Plus targeted tests:
    empty mask is identity-only (structured missing_capabilities body),
    requested-but-unheld bits are inert, and the legacy-metadata GraphQL probe
    (under a mask, an unreachable referent presents userCapability: null
    with null gated fields — legacy attenuates to null rather than leaking,
    and never authorizes; an accessible ref keeps its literal informational
    label per decision 3).
  • lifecycle — one minted token observed across snapshot refreshes as
    its user's grants change: a grant addition activates already-approved
    mask bits with no re-mint, and revocation takes effect at the next
    refresh. Both stale windows (before the refresh) are pinned deliberately:
    "immediately" means next-snapshot-refresh, no restart, no token
    invalidation.
  • guards_and_compat — the full-authority credential loop
    (createRefreshToken → exchange → full authority, including the dot-less
    bearer-credential form) closed by the empty-mask identity probe: an
    identity-only minted token still revokes its user's refresh token
    (revocation never widens), and the revoked credential is dead.

Task-7 assertions already pinned elsewhere are deliberately not repeated,
and the suite's module doc points at each: a minted token cannot re-mint
and the mint refuses service accounts (token_exchange), masked
createRefreshToken refusal and masked-revocation openness
(graphql/refresh_tokens), and the /admin fail-closed guards
(create_data_plane / update_l2_reporting).

Test infrastructure

  • test_server::RefreshableSnapshot — a manual snapshot watch the test
    refreshes by hand (via tokens::manual), so a single running server
    observes DB grant mutations the way production observes a periodic
    snapshot refresh. Fetches via try_fetch directly to skip
    PgSnapshotSource's MIN_REFRESH_INTERVAL cool-off.
  • fixtures/masked_suite.sql — a purpose-built grant graph, one edge per
    walk behavior under a mask (direct grant / Delegate hop / Assume hop /
    no path), documented in-file. alice.sql is untouched.
  • fixtures/jwt_sign_polyfill.sql — enables the SQL
    generate_access_token mint inside a sqlx::test database by overriding
    internal.access_token_jwt_secret() to the harness key and supplying a
    minimal HS256 sign() over pgcrypto's hmac(). This unlocks the
    credential loop's real exchange (previously impossible under sqlx::test —
    see the coverage note in graphql/refresh_tokens.rs); the polyfill's
    correctness is proven by SQL-signed tokens passing real Envelope
    verification.

Decisions (grilled 2026-08-28)

  1. Charter: strictly real-mint composition — every assertion exercises a
    genuinely minted token; unit-level duplication of 3c/5/6 coverage is out
    of scope. The suite is the regression net for mint/enforcement drift.
  2. Stale assertions dropped: the task's "upgrade union never exceeds
    live grants" and "expired-upgrade re-mint" items predate the removal of
    upgrade_token (Gregor, 2026-08-27) and are not implemented. Their
    successor property — a minted token cannot re-mint itself — turned out
    to be already pinned inside task 5's test_capability_token_mint. The
    plan comment's task-7 text is amended accordingly.
  3. Lifecycle harness: a refreshable snapshot source (one server, live
    refresh) over the two-servers-over-one-DB alternative, pinning
    "immediately = next snapshot refresh" as the documented semantics.
  4. Fixture: a new companion fixture with a dedicated tenant graph
    designed per-assertion, rather than growing alice.sql (whose shape many
    existing snapshots pin) or inlining SQL per test.
  5. Layout: a #[cfg(test)] directory module grouped by harness need
    (minting / walk / lifecycle / guards+compat).
  6. Surfaces: /authorize/user/collection is the canonical walk surface
    (richest outcome shape; denial messages carry the caller's email, which
    doubles as the identity-copy-through probe). Surface-specific assertions
    keep their own endpoints. The full assertion×surface matrix was rejected
    as re-proving 3c's per-surface plumbing.
  7. Headline property as a sweep: one snapshot-gridded implication sweep
    over a mask family, catching any future mask-widens-something regression
    nobody thought to enumerate, plus targeted example tests for the named
    semantics (Delegate confinement, Assume containment, empty mask).
  8. Legacy metadata probe: the minted ["Viewer"]-mask writesTo
    contrast — accessible ref keeps its informational literal label,
    mask-denied referent attenuates to null metadata and null fields
    together.
  9. Compat centerpiece: the full credential loop (proving "unmasked
    still can" alongside the guards' "masked cannot"), with the
    revokeRefreshToken rider pinning that an identity-only token's
    identity operates. Discovered constraint: the SQL exchange needs
    vault/pgjwt, absent under sqlx::test — resolved with the
    jwt_sign_polyfill fixture rather than degrading the approved loop
    test.
  10. No explicit duplicates: a post-implementation scan removed every
    test whose only delta from an existing pinned test was token
    provenance — the standalone re-mint probe, the empty-mask standalone
    (it was verbatim the sweep's empty row), the /admin refusal test,
    and the loop's masked-createRefreshToken step. The module doc carries
    coverage pointers instead.

Closes the task-7 item of #3376's implementation plan.

@bbartman
bbartman force-pushed the bmb/3376-stack-9-no-amplification-suite branch 2 times, most recently from ffa2828 to caac98d Compare August 28, 2026 16:59
@bbartman
bbartman force-pushed the bmb/3376-stack-9-no-amplification-suite branch from caac98d to 85d2a4d Compare August 31, 2026 12:45
Task 7 of #3376: end-to-end assertions that a masked token's authority is
always a subset of its user's live grants. Every test drives a token
minted by the real capability_token grant through real routes over a
DB-backed snapshot: an insta-gridded implication sweep over an 11-mask
family, the lifecycle of one token across snapshot refreshes (via the new
test_server::RefreshableSnapshot), the legacy-metadata null-attenuation
probe, and the full refresh-token credential loop — whose SQL exchange
runs under a new jwt_sign_polyfill fixture supplying the HS256 sign()
and secret a sqlx::test database lacks. Assertions already pinned by
tasks 3c/5/6 are deliberately not repeated; the module doc carries
coverage pointers.
@bbartman
bbartman force-pushed the bmb/3376-stack-9-no-amplification-suite branch from 85d2a4d to 99fdc4d Compare September 1, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant