Hub-and-spoke skill that converts "I need auth for app X" into a reviewable plan across 5 phases: intake (flows/stack/storage/MFA), identity-provider pick + env scaffold, session strategy + cookies, authorization model + permission matrix, and threats + mitigations. - 8 AskUserQuestion calls total (≥6 hub-and-spoke contract; 4 in Phase 1 + 1 each in Phases 2–5). - Reads all four _blocks/auth-*.md; never writes production code or secret values. - RULE 0.8 (Secrets SSoT): emits env VARIABLE NAMES only; storage path is secrets/auth.env per domain-has-secrets.md. - Constructor Pattern: 6 files, largest 115 LOC (<200 limit). - Fail-closed default + NO DOWNGRADE on unsafe combinations (passkey-only without recovery → return recovery-path options, not "not supported"). Evidence grade [E2] — pipeline mirrors OWASP ASVS v4.0.3 chapters 2–4.
3.4 KiB
3.4 KiB
Phase 4 — Authorization model + permission matrix
Decides who-can-do-what after authentication. Reads
_blocks/auth-authorization.md. Fail-closed by default.
4a — Model click (AskUserQuestion, single-select)
{
"questions": [
{
"question": "Authorization model?",
"header": "Authz",
"multiSelect": false,
"options": [
{"label": "RBAC (roles → permissions)",
"description": "Static roles (admin / editor / viewer). Simplest, enough for most apps with <5 roles."},
{"label": "RBAC + ownership",
"description": "Roles + per-row owner_id check. The sweet spot for multi-tenant SaaS."},
{"label": "ABAC (policy engine)",
"description": "Attributes + context (time, IP, resource tier). Use Cerbos or OPA. Adopt when rule count >~20."},
{"label": "ReBAC (Google Zanzibar)",
"description": "Graph-shaped sharing (folders/teams/orgs). SpiceDB or OpenFGA. Pick only if your domain is inherently graph-shaped."},
{"label": "None — single-user app",
"description": "No authz layer beyond authentication. Record explicitly."}
]
}
]
}
Store as AUTHZ.
4b — Emit permission matrix skeleton (inline)
For RBAC / RBAC + ownership, emit a table stub the user must fill in
before coding. Example for a notes app:
| Role | notes:read | notes:write | notes:delete | users:manage |
|--------|:---------:|:-----------:|:------------:|:------------:|
| admin | all | all | all | yes |
| editor | owned | owned | owned | no |
| viewer | owned | no | no | no |
- Columns =
resource:actiontokens — these become thePermissionenum variants in code. - Cells =
all/owned/no/shared:<relation>— NEVER free-text. - Save as
docs/permissions.mdin the target repo; treat it as code (tested, reviewed, versioned).
4c — Enforcement-point rule (inline)
- Middleware, not handlers. Every authenticated request runs an authz
decision BEFORE the handler sees it. Handler receives a typed
AuthorizedRequest<Action, Resource>or the request 403s earlier in the stack. - Ownership checks enforced in BOTH the middleware AND the data layer
(
WHERE tenant_id=$1 AND owner_id=$2). Double layer defeats IDOR. - Fail-closed: unknown action, missing role, policy-engine error → 403. Log every denial with subject + action + resource + reason.
- Audit log append-only row on every privilege change, role assignment, and denied action. Required for SOC2 / HIPAA / ISO 27001.
4d — Policy-engine pick (inline, driven by AUTHZ)
RBAC/RBAC + ownership→ in-code match onPermissionenum; no engine.ABAC→ Cerbos (YAML rules, stateless decision service) OR OPA/Rego (general-purpose, steeper curve). Keep.cerbos.yaml/.regofiles in the repo, unit-tested like code.ReBAC→ SpiceDB (Zanzibar reference) OR OpenFGA (Auth0-backed). Define the schema, seed relationships, use the client SDK.None→ emit a single line "authz skipped — no multi-user model".
Verify-criterion
AUTHZis exactly one choice.- If RBAC chosen, permission-matrix skeleton with ≥1 row + ≥1 column is printed.
- Enforcement-point rule (4c) is emitted verbatim — non-negotiable.
- If a policy engine is implied by
AUTHZ, the pick is named (Cerbos / OPA / SpiceDB / OpenFGA).