Lesson 11 · Row-Level Security & the whole chain
Defense in depth — the full trace
One real request, followed through every gate — and, at each one, what it would catch if all the others failed.
Your win: trace a real endpoint from token to rows through all six gates, and for each gate say precisely what it enforces and what independent protection it provides — the concrete meaning of "defense in depth."
The request: a parent reads their children
We'll follow RetrieveStudentAssociatedToParentAccount — a Parent-only endpoint that
returns the caller's own children. It touches every gate cleanly.
Gate ④ is the quiet hero
Look closely at the handler. It does not take the parent id from the request — it reads the caller's own id from the verified context:
userID := interceptors.UserIDFromContext(ctx) // ← the CALLER, from the JWT — not req
parentID.Set(userID)
users, _ := urs.StudentRepo.GetStudentsByParentID(ctx, urs.DB, parentID)
What each gate catches — the whole point of layering
Defense in depth means: assume any one gate has a bug, and ask what still protects the data.
| Gate | Enforces | If every other gate failed, it still stops… |
|---|---|---|
| ② Verify | authentic identity | …an anonymous or forged-token caller — no identity, no entry |
| ③ Role gate | the caller's role may call this method | …a Student or Teacher calling a Parent-only endpoint |
| ④ Ownership | you act as yourself | …a parent asking for another parent's children (it's not expressible) |
| ⑤ Repo | the query's own filters | …soft-deleted rows, wrong-parent rows |
| ⑥ RLS | tenant + permission + self, in the DB | …cross-tenant rows even from a raw query or a forgotten check |
No single failure breaches the data. Forget the repo filter? RLS still scopes the tenant. Bug in RLS? The handler still only asked for the caller's own children. Skip the role gate? An anonymous caller never got past verification. Each gate is independent, and each is a full stop on its own axis. That redundancy is the design — not accidental belt-and-suspenders, but the deliberate assumption that some layer will eventually have a bug.1
resource_path
Follow one value across the whole trace. It's minted into the JWT by Shamir (L3), verified and put
on the context by the interceptor (L4), read by setPostgres into a session variable
(L9), and checked by permission_check in every RLS policy (L9–10). One claim field,
threaded through five layers, is what makes a multi-tenant platform safe. That's the spine of this
whole course.
Defense in depth & least privilege
The security principle this trace embodies — layered, independent controls so no single failure is catastrophic.
→ NIST — defense in depth
→ in-repo trace: …/user_reader_retrieve_student_associated_to_parent.go:15 ·
.claude/rules/security.md (the layered rules)
Check yourself (from memory)
Q1. In the parent endpoint, where does the "whose children?" parent id come from?
UserIDFromContext(ctx). There's no parent id to forge —
the unsafe request simply can't be expressed.
Q2. If a bug let a Student past the role gate, what would still stop them reading a parent's children?
Q3. What is the single value threaded through every gate of the trace?
setPostgres sets it → permission_check enforces it. One field, five layers.
RetrieveStudentAssociatedToParentAccount):
① token (JWT in md) → ② verify (interceptor, vs JWKS → ctx claims) → ③ role gate
(rbacDecider={RoleParent}) → ④ ownership (handler:
UserIDFromContext as parentID — never from req) → ⑤ repo
(WHERE parent_id=$1 AND deleted_at IS NULL) → ⑥ RLS (setPostgres session
vars → tenant + location-permission + self policies). Defense in depth: ② stops
anonymous/forged; ③ stops wrong-role; ④ makes "another parent's children" inexpressible;
⑤ filters soft-deleted/wrong-parent; ⑥ stops cross-tenant even from a raw query. No single failure
breaches data. Through-line: resource_path — minted by Shamir →
verified by interceptor → set by setPostgres → checked by permission_check.rls_scan that
catches a missing policy, service-to-service auth, the repo's surprises, and the whole-course
recap. Ask me to re-run this trace on a different endpoint if you'd like the reps.
1. NIST — defense in depth. In-repo: internal/usermgmt/modules/user/core/service/user_reader_retrieve_student_associated_to_parent.go:15-22, internal/usermgmt/modules/user/adapter/postgres/repository/legacy_student.go:185.