---
id: w4/m8/t001
title: "Session-caller path: confirm/extend GraphQL api-key ops for Kratos-session users (interplay with m4's checker)"
worker: worker4
status: done
estimate: 30m
depends_on: []
---

## Objective

Make sure a Kratos-session caller (the dashboard) can actually drive the m3 api-key GraphQL operations: verify the session-auth path reaches mint/list/revoke (m3 added session-cookie gating during /simplify — confirm which verbs it covers), extend where it does not, and settle how session identities are authorized once m4's `Checker` enforces — the session subject (`api.IdentityFrom`) needs tuples or an explicit design note so the dashboard surface will not 403 the day enforcement flips.

## Context

- m3 shipped: API-key verbs in Core + REST `/v1/api-keys` + GraphQL + MCP; list/revoke scoped to bex-minted keys via the `bex.co/api-key` metadata marker; session-cookie gating applied during m3 t006.
- m4 (the `Checker` seam + seed tuples) is done (2026-07-06) — the tuple story below is against the shipped model, not a moving target.
- `operator/internal/api/CLAUDE.md` before editing; `docs/ADR006-bex-api.md` for the adapter conventions.

## Steps

1. Trace the GraphQL api-key resolvers' auth requirements for a session (not bearer) caller; write a failing-or-passing check first.
2. Extend the session path where a verb is bearer-only; keep Core logic shared.
3. Record (code comment or docs/ADR012-auth.md note) how session subjects map to checker tuples, aligned with m4's model.
4. `make test` green (GOTOOLCHAIN pin per memory).

## Files

- `operator/internal/api/` (GraphQL adapter, auth middleware as needed)
- `docs/ADR012-auth.md` or `docs/ADR006-bex-api.md` (session-subject authorization note)

## Acceptance criteria

- [x] A Kratos session can mint, list, and revoke via GraphQL on the mock cluster (or httptest-level equivalent).
- [x] The m4 tuple story for session subjects is written down; no contradiction with the checker design.

## Out of scope

- The dashboard UI itself (t002/t003); changing m4's model.

## Resolution

`oryAuth.middleware` (`internal/api/auth.go`) already resolves a session cookie/`X-Session-Token` to `core.Identity{Method: "session"}` and attaches it to the request context exactly like a bearer's `Method: "oauth2"` identity; `apikeys`' GraphQL resolvers call `p.Context` straight through to `Service.{Create,List,Revoke}APIKey`, which gate on `s.Authorize(ctx, core.RelCanManageKeys)` — there is no bearer-only branch anywhere in the path, so no code change was needed, only proof. Added `TestAPIKeys_SessionCaller` (`internal/api/server_test.go`) covering: an authorized session mints/lists/revokes over GraphQL and is checked as `user:identity-1` / `can_manage_keys` (same shape a bearer subject gets); a session without the workspace tuple is 200-with-`forbidden` (GraphQL's error convention), not silently allowed; no credential at all is 401 before the resolver runs. Documented the tuple story in `docs/ADR012-auth.md`'s Authorization section: session and bearer subjects are checked identically, but only `bex-bootstrap` is seeded by `authz-model.sh` — a human's Kratos identity needs a `user:<kratos-id>` tuple written manually until w1/m2's members API exists, otherwise the dashboard's API-key UI (t002/t003) will 403 for any session that hasn't been granted one.
