Files
pad/internal
xarmian 3f6bcc0ff2 feat(oauth): per-connection state tables + dual-read introspection gate (TASK-1520) (#581)
* feat(oauth): per-connection state tables + dual-read introspection gate (TASK-1520)

Phase A foundation for PLAN-1519 / IDEA-1517's per-OAuth-connection state
overhaul. Promotes the consent-time workspace allow-list out of
session.Extra (per-token, re-minted on every refresh-token rotation) into
dedicated tables keyed by request_id (the grant chain identifier preserved
across rotations).

Schema (SQLite migration 059 + Postgres migration 038):
- oauth_connections: one row per grant chain with name + three scope flags
  (may_create_workspaces, all_current_workspaces, include_future_workspaces).
- oauth_connection_workspaces: mutable allow-list join table; PK on
  (request_id, workspace_id); FK ON DELETE CASCADE; added_by audit column.

Store (internal/store/oauth_connections.go): Create/Get/Rename/SetScopeFlags/
Add+Remove+IsAllowed/Delete CRUD. GetOAuthConnectionAccess is the hot-path
projection — one PK lookup + one indexed join when the wildcard flag is off,
nothing else when it's on.

Dual-read gate (internal/server/middleware_mcp_auth.go): OR-merges the
legacy session.Extra allow-list with the new-table projection. A workspace
is allowed iff either source allows it; either source's wildcard makes the
gate unrestricted. New tables stay empty until Phase C writes the consent
screen, so the dual-read is a no-op until then — and existing OAuth grants
keep working unchanged through the Extra path. I/O errors on the new path
fall back to the Extra path so a transient outage of the new tables can't
regress existing connections.

Tests:
- 10 store tests cover CRUD, FK cascade, wildcard short-circuit, sorted
  slug projection, idempotent add/remove, ErrOAuthConnectionNotFound on
  missing rows.
- 11-case table-driven test on mergeAllowedWorkspaces directly verifies
  PLAN-1519's acceptance criterion: "token with allow-list in session.Extra
  still passes; token with empty session.Extra but row in
  oauth_connection_workspaces also passes." Plus wildcard precedence, union
  dedup, and fail-closed-on-empty-scope.
- BenchmarkMergeAllowedWorkspaces measures policy-function overhead on the
  hot path (the store-side lookup is the other half of the dual-read cost).

Parent: PLAN-1519.

* fix(oauth): fail-closed on connection lookup error per Codex review (round 1)

PR #581 Codex review round 1 caught two issues:

1. middleware_mcp_auth.go: GetOAuthConnectionAccess errors fell through
   to "no connection" + nil allow-list = unrestricted. Post-Phase-C
   (when the new tables are authoritative and session.Extra is empty),
   a DB read error on a scoped token would silently grant access to
   every workspace the user belongs to. Now fails closed with a 401
   matching the IntrospectToken storage-error policy, increments
   MCPAuthzDenialsTotal{connection_lookup_error} for ops visibility.

2. oauth_connections.go: CreateOAuthConnection's docstring claimed
   "scope flags default ON if not supplied," but the method writes
   the three Go bools verbatim — and Go zero-values for bool are
   false, not true. The schema-level DEFAULT TRUE is unreachable
   through this path. Docstring updated to clarify that defaults
   live at the form-rendering layer; the store is a faithful
   pass-through.

Parent: PLAN-1519.

* test(oauth): add store-side bench for GetOAuthConnectionAccess per Codex review (round 2)

PR #581 Codex review round 2 flagged the docstring references to
bench_oauth_connections_test.go pointing at a file that didn't exist
— only the in-memory mergeAllowedWorkspaces bench was wired. Add the
store-side bench so the documented file is real and PLAN-1519 Phase
A's "Hot-path benchmark: dual-read overhead measured and documented"
acceptance bullet is satisfied end-to-end.

Three shapes covered: Wildcard (PK lookup, join short-circuited),
Explicit (PK + indexed scan + small workspaces join), and NoRow (the
dominant Phase-A path until Phase C wires the write path). Local
numbers (Ryzen 3 5300U, SQLite WAL): 12µs/12µs/37µs respectively —
all comfortably sub-millisecond.

Parent: PLAN-1519.
2026-05-18 00:03:20 -04:00
..
2026-03-26 01:52:36 +00:00