# Authorization

> Design access control — RBAC for coarse function-level checks, Postgres Row Level Security (RLS) for row-level data isolation, ABAC pushed to the app/policy layer. Use when adding permissions, building multi-user data access, or when one user can see another's data. Not for establishing who the caller is (use authentication) or tenant isolation specifically (use multitenancy-audit).

- Skill: `jaykim88/authorization` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/authorization`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/authorization/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/authorization

---


# Authorization

## Purpose
Control what an authenticated caller can do and which rows they can touch — using the right mechanism at the right layer so a missing check can't leak another user's data (the #1 API vulnerability).

**Universal** — RBAC vs ABAC, the function-level-vs-row-level distinction, and "enforce at the data layer when possible" are authz principles; Postgres RLS is the default enforcement primitive.

## Procedure

1. **Separate coarse (function) from fine (row) authorization**
   - **Function-level (RBAC)**: "can this role call this endpoint?" — roles + permission checks at the API layer
   - **Row-level (data isolation)**: "can this user see THIS row?" — enforce at the DB with RLS
   - Most BOLA bugs come from checking function-level but forgetting row-level

2. **Use RBAC for role/permission gates**
   - Define roles → permissions; check at the controller/guard layer
   - Keep roles coarse; avoid permission explosion

3. **Use Postgres RLS for row-level isolation**
   - Enable RLS on user-data tables; write policies (`USING` / `WITH CHECK`)
   - The DB enforces it even if the app forgets a `WHERE user_id = ?` — defense in depth
   - Inject the user/tenant context per request (`SET LOCAL app.current_user`) inside the transaction

4. **Push ABAC (attribute/context rules) to the app or a policy engine**
   - RLS struggles with complex attribute/context-aware rules (time-of-day, resource state, relationship graphs)
   - For those, use an app-layer policy or a dedicated engine (OPA/Oso/Cerbos)
   - Don't contort RLS into doing ABAC

5. **Check authorization at the right layer — never client-only**
   - Client-side hiding of buttons is UX, not security
   - Every mutating/reading endpoint enforces server-side (RBAC) + DB (RLS)

5b. **Verify ownership at every level of a nested resource**
   - `/orgs/:o/teams/:t/members/:m` — checking only that the caller owns `:o` lets them touch any `:t` or `:m`. Re-verify the chain: `m ∈ t ∈ o ∈ caller-orgs`
   - RLS predicates that *only* check the leaf table miss the parent relationship — write the policy against the full join, or enforce at the controller as well

5c. **Mind the cost of per-request authorization**
   - A permission check that hits the DB on every request becomes an N+1-shaped bottleneck under load
   - Cache the decision briefly (per-request memoization for repeated checks in one request; a short TTL session cache for stable permissions). The cache is an *optimization* — RLS still enforces at the DB

6. **Validate (validation loop)**
   - As user A, attempt to read/modify user B's resource by ID → verify denied (BOLA test)
   - Disable RLS temporarily in a test → verify the app-layer check still denies (defense in depth)
   - If cross-user access succeeds → row-level check missing; add RLS policy + re-test

## Anti-patterns

| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Function-level check only (no row check) | RBAC (function) + RLS (row) together |
| Relying on app `WHERE user_id = ?` alone | RLS as defense-in-depth at the DB |
| Forcing ABAC rules into RLS policies | Push attribute/context rules to app/policy engine |
| Client-side permission enforcement | Server + DB enforcement |
| `RLS USING (true)` (effectively disabled) | Real tenant/user predicate |
| Checking ownership only at the parent of a nested resource | Verify the full chain (`m ∈ t ∈ o ∈ caller-orgs`) |
| Permission check hitting DB on every request | Per-request memoization or short-TTL session cache (RLS still enforces at DB) |

## Severity tiers

| Tier | Examples | Action SLA |
|---|---|---|
| **Critical** | BOLA — user can access another user's data by changing an ID; RLS disabled or `USING (true)` on a user-data table | Block release; fix immediately |
| **Major** | Function-level check present but row-level missing; ABAC rule enforced only client-side | Fix this sprint |
| **Minor** | Over-granular roles; policy duplication between layers | Schedule within 2 sprints |

## Stop & Ask (AI must pause for user approval)

- **Before enabling or disabling RLS on a populated table** — a missing default policy can hide or leak all rows
- **Before changing a policy's `USING` / `WITH CHECK` expression** — confirm the new predicate is at least as restrictive (or intentionally widened, with reasoning)
- **Before altering role definitions on a production-active role** — in-flight sessions may break

## Completion Criteria
- [ ] RBAC checks at the API layer for function-level access
- [ ] RLS enabled + correct policies on all user-data tables
- [ ] ABAC rules in app/policy layer, not contorted RLS
- [ ] BOLA test passes (cannot access another user's rows)
- [ ] No client-only authorization

## Output
- **Authz implementation**: RBAC guards + RLS policies
- **Policy doc**: roles → permissions matrix + RLS policy list
- **Commit format**: `feat(authz): RLS policy for <table>` / `fix(authz): add row-level check to <endpoint>`

## Implementation

### TypeScript + Supabase / Postgres + NestJS (default)
- **RLS (Supabase)**: `ALTER TABLE x ENABLE ROW LEVEL SECURITY; CREATE POLICY ... USING (auth.uid() = user_id)`
- Custom claims RBAC: Supabase JWT custom claims → role checks
- NestJS: `@UseGuards(RolesGuard)` for function-level; RLS handles row-level
- Tenant context: `SET LOCAL` inside the request transaction (see `multitenancy-audit`)

### Other stacks
- **Python / FastAPI**: dependency-injected permission checks; Postgres RLS via SQLAlchemy `SET LOCAL`
- **Go**: middleware RBAC; RLS via `SET LOCAL` per request; Casbin for policy
- **Universal**: RLS is Postgres (also in other RDBMS); the RBAC/ABAC/row-vs-function distinction is universal; OPA/Oso/Cerbos are language-agnostic policy engines

## Related skills
- `authentication` — authz comes after authn establishes identity
- `multitenancy-audit` — tenant isolation is RLS applied to a tenant_id
- `backend-security-audit` — broken object-level authz (BOLA) is OWASP API #1

## Reference
- **Key insight encoded**: Use RBAC (roles) for coarse function-level checks and RLS for row-level data isolation; RLS struggles with attribute/context-aware (ABAC) rules, so push those to the app or a policy engine.

