Quickstart / Validation Scenarios: Service Accounts
End-to-end scenarios that validate the spec's acceptance criteria. Use these as the manual smoke test and as the basis for integration tests. Assumes a dev stack (docker-compose) with Keycloak, OpenFGA, MongoDB, BFF, DA backend, and AgentGateway + the OpenFGA ext_authz bridge enabled.
Preconditions
- A test user Alice in team
team-srewithcan_use agent:incident-resolverandcan_call tool:jira/search(but NOTtool:github/create_issue). - A second user Bob in team
team-marketingonly (no overlap with Alice). - AgentGateway bridge enabled with
CAIPE_AGENT_CONTEXT_HMAC_SECRETset (so tool checks run).
S1 — Create a scoped service account (US1; FR-001..FR-009)
- As Alice, open Admin → Settings → Service Accounts → Create.
- Confirm the owning-team dropdown lists team-sre (Alice's team).
- Confirm the scope picker offers incident-resolver and jira/search, and does NOT offer
github/create_issue(Alice lacks it). (FR-009) - Name it
incident-bot, select both offered scopes, submit. - Expect: 201; credential (
client_id+client_secret+ token URL) shown once. (FR-005) - Dismiss the dialog, reopen the SA → Expect: no way to see the secret again. (FR-005)
Pass: SA created, owned by team-sre, two scope tuples written, secret revealed once.
S2 — Default-deny on creation (FR-004)
Create empty-bot with NO scopes selected → Expect: created with zero grants; any later call it
makes is denied until scopes are added.
S3 — Permission bound is enforced at write time (FR-006/008)
Via API (bypassing the UI), POST /service-accounts with a scope Alice lacks
(tool:github/create_issue) → Expect: 403/rejected, that scope not written, response names the
rejected scope.
S4 — Use the service account from outside (US2; FR-010/011)
POST {token_url}withgrant_type=client_credentials+ the SA's client_id/secret → get JWT. Confirmsub= the SA's UUID andpreferred_username=service-account-caipe-sa-incident-bot-….POST {CAIPE_API_URL}/api/v1/chat/invokewithAuthorization: Bearer <JWT>targetingincident-resolver.- Expect: authenticated as
service_account:<sub>, agent-use allowed, agent runs. (validates the WS-G DA fix — without it this wrongly 403s)
S5 — Agent access does NOT leak tool access (US2; FR-012 — validates WS-F)
- Grant the SA
agent:incident-resolverbut NOTtool:jira/search(remove it if present). - Have the agent attempt a
jira/searchtool call under the SA's token. - Expect: tool call denied (
DENY_CALLER_TOOL) even thoughagent:incident-resolver can_call tool:jira/search— because the SA caller lacks the tool grant. - Add
tool:jira/searchto the SA; retry → Expect: allowed (both agent and caller authorized).
S5b — Human-user regression (the pre-existing gap) (FR-012b — validates WS-F for users)
- As a user with
can_use agent:incident-resolverbut NOTcan_call tool:jira/search, invoke the agent so it would calljira/search. - Expect (post-fix): tool call denied for the human user too. (Pre-fix this leaked.)
S6 — Manage scopes after creation (US3; FR-015/016)
- As Alice (owning-team member), add
tool:jira/searchshe holds → Expect: succeeds. - Add
tool:github/create_issueshe does NOT hold → Expect:403. (FR-015) - Remove a scope (any), including one added by another member → Expect: succeeds unconditionally. (FR-016)
- Confirm the SA's credential is unchanged throughout. (FR-019)
S7 — Rotate (US4; FR-017/019)
Rotate incident-bot → new secret shown once; old client_secret no longer obtains a token; scopes
unchanged.
S8 — Revoke is terminal (US4; FR-018/018a)
Revoke incident-bot → its token no longer authenticates; all its OpenFGA tuples gone; it leaves the
active list; the Mongo doc is retained (status: revoked); the name incident-bot can be reused by
a new SA in team-sre. (FR-018a)
S9 — Ownership boundary (US5; FR-021/022)
As Bob (team-marketing only), open Service Accounts → Expect: Alice's incident-bot is NOT
visible; direct API GET/DELETE on it → 403/404. No way to share it to another team. (FR-022)
S10 — Static access on creator permission loss (FR-020)
Remove Alice's own can_use agent:incident-resolver. The SA's service_account:<sub> can_use agent:incident-resolver tuple is unchanged; the SA still works. (FR-020)
S11 — Team deletion guard (FR-025)
Attempt to delete team-sre while it owns incident-bot → Expect: blocked with a clear message;
deletion succeeds only after the SA is revoked.
S12 — Audit trail (FR-026/027; SC-009)
After S1, S6, S7, S8: confirm audit records exist for create / scope_add / scope_remove / rotate / revoke (actor + target + scope). After S4/S5: confirm call-time allow/deny decisions are audited (service account + resource). (FR-027)
Automated test mapping
- Bridge (pytest) —
deploy/openfga/bridge/tests/test_grpc_bridge.py: add cases for S5/S5b (caller-keyed deny for user + service_account; allow when both granted). - DA backend (pytest) — service-account subject namespacing in
openfga_authz.py(S4). - BFF (Jest) —
service-accounts.tslib + each route: create/list/detail/rotate/revoke/scopes, the write-time permission bound (S3/S6), and ownership gating (S9). - Model — a check that
service_account:<sub> can_manageresolves fromowner_team.