Lesson 12 · Row-Level Security & the whole chain

rls_scan, service-to-service & the recap

The guardrail that catches a missing policy, how the mesh authenticates itself — and the whole course in one view.

Your win: explain how the repo guards against a forgotten RLS policy, how internal service-to-service calls get an identity, and recite the entire auth/authz chain from memory.

The guardrail: rls_scan

RLS only protects a table if someone remembered to add the policy. A new table shipped without one is a silent cross-tenant leak — no error, just data visible to everyone. So a daily job audits it:

cmd/server/rls_scan/rls_scan.go + internal/golibs/database/scan_rls.go:11
ScanRLS:  for every table in schema public:
            does it have the expected permission_check RLS policy?
          if not, and it's not in migrations/public_tables.json (the allow-list) → FLAG IT
Why a scanner, not just discipline Every tenant table must have RLS — but "must" enforced only by code review eventually fails. rls_scan enumerates every table and checks each has a permission_check policy, comparing against an explicit allow-list of intentionally-public tables (public_tables.json). A table that's neither protected nor allow-listed gets flagged. It's the automated backstop for the one human error the whole design can't otherwise catch: forgetting. (This is the same spirit as the deployment track's drift detectors — verify the desired state, don't just hope for it.)

Service-to-service: identity without a human

Not every call comes from a logged-in person. When conversationmgmt consumes a Kafka event, or one service calls another internally, there's no user JWT to verify — yet the downstream code still needs an identity (for RLS's resource_path, if nothing else). Two mechanisms fill the gap (both met earlier):

Faked JWT context (L4)

FakeJwtContext synthesizes a School-Admin CustomClaims from the request's organization_id for allow-listed internal methods. The org scopes the call; the mesh boundary is the trust.

Shamir fake tokens

GenerateFakeToken mints real, signed Manabie tokens for non-prod and testing — a genuine token without a real login, for controlled environments.

The risk being managed — the confused deputy A service acting on behalf of internal callers is a classic confused-deputy hazard: if it accepted any org from any caller, an attacker could make it act across tenants. The mitigation is scope: the faked identity is pinned to the organization_id in the request, and these methods are a small, explicit allow-list — never a business endpoint. (Recall Lesson 4: fakeJwtCtxEndpointignoreAuthEndpoint; synthesized identity, not no identity.)

The edges worth remembering

Things that will trip you (or an interviewer) if you don't know them:

SurpriseThe reality
"Missing rbac entry panics at startup"Myth. It's PermissionDenied at runtime (L5)
Two interceptors packagesgolibs (legacy) & usermgmt (live); same type names (L1, L4)
"Shamir" = secret sharingNo — it's a JWT authority; standard RSA signing (L3)
Duplicate claims in a tokenmanabie.* and x-hasura-* — for Go and Hasura RLS (L2)
Auth path depends on a flagUnleash DecouplingUserAndAuthDB switches bob-DB vs a separate auth-DB per org

🏛️ The whole chain, in one view

AUTHENTICATION (who are you?) — Part 1 login (Firebase) → Shamir exchange+mint (JWKS) → JWT{ manabie: user_id, roles, RESOURCE_PATH } → interceptor: verify vs JWKS (sig/iss/aud/exp) → stamp identity on ctx [ignore / fakeJwt lists] AUTHORIZATION (what may you do?) — Parts 2–3, three tiers ① ROLE rbacDecider[method] → role allow-list (nil=any; absent=PermissionDenied, NOT panic) ② PERMISSION+LOCATION user→group→role→permission, scoped to a location SUBTREE (access_path ~~) resolve: FindLocationIDsByUserIDAndPermissionName · gate: LocationRestricted ③ RLS setPostgres stamps resource_path/app.user_id → permission_check (tenant) + location-aware policies (permission-or-self) · FORCE RLS · rls_scan guards it DEFENSE IN DEPTH — each gate independent; resource_path threads through all of them GUARDS — rls_scan (missing-policy drift) · faked JWTs (service-to-service, org-scoped)
The five ideas that carry the interview 1. authN vs authZ — verify a JWT (who), then three tiers of what. 2. Token exchange — Shamir turns a federated login into a first-party JWT + hosts the JWKS everyone verifies against. 3. RBAC → ABAC — a coarse role gate, then permissions scoped to a location subtree via materialized paths. 4. RLS — the DB itself isolates tenants (and locations) from a session variable set from the token; FORCE-d so even the owner is bound. 5. Defense in depth — the same rules enforced at independent layers, with a scanner guarding the one thing humans forget.
Read this next

The confused deputy & token exchange for services

The service-to-service hazard, and the standard (RFC 8693) way to carry identity between services — the ideas behind the faked-JWT design.

RFC 8693 — OAuth 2.0 Token Exchange (delegation/impersonation)
The confused deputy problem · in-repo cmd/server/rls_scan/rls_scan.go, internal/golibs/interceptors/internal_api.go

Check yourself (from memory)

Q1. What does rls_scan protect against?

It enumerates every public table and flags any missing the permission_check policy (unless allow-listed). The backstop for "forgetting."

Q2. How does an internal, human-less call (e.g. a Kafka consumer) get an identity?

FakeJwtContext fabricates org-scoped claims for allow-listed internal methods — synthesized identity, not none.

Q3. A missing rbacDecider entry causes…

The recurring correction: Check returns sttNoDeciderProvided at call time. No startup validation exists.
Recall: the guardrails + the whole-course spine.
scanner + s2s + the chain, then reveal
rls_scan (cmd/server/rls_scan, daily): enumerates every public table, flags any without permission_check RLS (unless in public_tables.json) — catches the forgotten-policy tenant leak. Service-to-service: FakeJwtContext synthesizes org-scoped School-Admin claims for allow-listed internal methods; Shamir GenerateFakeToken for non-prod. Manages the confused-deputy risk via org-scoping + a small allow-list. The chain: authN (Firebase → Shamir mint/JWKS → JWT{resource_path} → interceptor verify → ctx) then authZ 3 tiers: ① role (rbacDecider), ② permission scoped to a location subtree, ③ RLS (permission_check tenant + location-aware, FORCE-d). Defense in depth; resource_path threads all layers. Edges: rbac-panic myth, two interceptor packages, Shamir≠secret-sharing, Hasura twin, Unleash DB-decoupling.
🎓🏁 Course complete — Authentication & Authorization Twelve lessons: authentication (the JWT + Manabie claim, Shamir minting it, the interceptor verifying it) → authorization (the role gate, granular permissions, the location tree) → Row-Level Security (tenant + location, defense in depth, the guardrails). You can now open any endpoint and trace it from a token to a tenant-isolated row — and name what the repo really does: the rbac-panic myth, the two interceptor packages, the Shamir name red-herring, the Hasura twin. That's genuine, interview-grade fluency in the most security-critical machinery in the codebase.
That's the build. The one thing that would prove it stuck is a retention check: ask me to run a mock interview across this course (authN → roles/permissions/locations → RLS, cold, no peeking) and I'll write the first real learning records showing where you're solid and where to review. You've built the knowledge; let's confirm it's yours. Ask me to run it.

1. In-repo: cmd/server/rls_scan/rls_scan.go, internal/golibs/database/scan_rls.go:11, internal/golibs/interceptors/internal_api.go. RFC 8693.