Lesson 10 · Row-Level Security & the whole chain

Location-aware RLS policies

RLS enforces more than the tenant — some tables push the whole permission-on-a-location check (Lesson 8) into the database.

Your win: read a policy that combines tenant + permission + location, explain how Postgres combines permissive and restrictive policies, and see why this is Lesson 8's app-layer check re-enforced in the database.

Beyond the tenant: permission on a location, in SQL

Lesson 9's policies checked one thing: resource_path = your tenant. But the most sensitive tables — users, locations, user_access_paths — go further. Their policies check whether you (app.user_id) hold the right permission on the row's location — the exact question Lesson 8 answered in Go, now answered by the database. Here's the read policy for the users table:

migrations/bob/1421_migrate.up.sql:11 (rls_users_select_location, condensed)
CREATE POLICY rls_users_select_location ON users AS PERMISSIVE FOR select USING (
  EXISTS (
    SELECT 1 FROM granted_permissions p
      JOIN user_access_paths usp ON usp.location_id = p.location_id
    WHERE p.user_id = current_setting('app.user_id')                  -- ← ME (the caller)
      AND p.permission_id = (SELECT permission_id FROM permission
                            WHERE permission_name = 'user.user.read'       -- ← the permission
                              AND resource_path = current_setting('permission.resource_path'))
      AND usp.user_id = users.user_id ))                            -- ← the row's user, at that location

Read it in English: "this users row is visible if I hold user.user.read on a location that the row's user belongs to." That's granted_permissions (Lesson 6) joined to user_access_paths (Lesson 7), keyed by app.user_id and permission.resource_path — the session variables setPostgres set in Lesson 9. Every piece of the last five lessons meets here.

The self-visibility policy

There's a second policy on the same table, and it's a lovely detail:

migrations/bob/1422_migrate.up.sql:4 (rls_users_permission_v4)
CREATE POLICY rls_users_permission_v4 ON users AS PERMISSIVE FOR ALL
  USING      ( current_setting('app.user_id') = user_id )
  WITH CHECK ( current_setting('app.user_id') = user_id );

"You can always see (and edit) your own user row." Without this, a user with no user.user.read permission couldn't even read their own profile. So a users row is visible if you have permission on its location OR it's you. That "OR" is where Postgres policy combination matters.

The key mechanic: permissive OR, restrictive AND

A table can have many policies. Postgres combines them by a precise rule:1

The combination rule (memorize this) A row is accessible if at least one PERMISSIVE policy allows it AND every RESTRICTIVE policy allows it. Permissive policies are OR-ed together (any grant opens the door); restrictive policies are AND-ed (all must agree). Think: permissive = "reasons you're allowed," restrictive = "conditions that must always hold."

So the users table composes like this:

VISIBLE( users row ) = ( rls_users_select_location [PERMISSIVE: I hold user.user.read on the row's location] OR rls_users_permission_v4 [PERMISSIVE: the row IS me] ) AND ( rls_users_restrictive [RESTRICTIVE: row.resource_path = my tenant] ) └──────────────────── permission-or-self, within my tenant ────────────────────┘

The locations table follows the same shape: a permissive rls_locations_location (you hold master.location.read on it) AND a restrictive rls_locations_restrictive (tenant match) (migrations/auth/1001:544,559). Permission opens the door; the tenant is the wall that always stands.

This is Lesson 8, again — on purpose The app-layer FindLocationIDsByUserIDAndPermissionName (Lesson 8) and rls_users_select_location encode the same rule: "you may only touch users at locations where you hold user.user.read." One runs in Go before the query; the other runs in Postgres during it. Neither trusts the other. A handler that forgets the app-layer check is still caught by RLS; a misconfigured policy is still narrowed by the app check. The same rule, enforced twice, independently — that's what "defense in depth" concretely means, and it's the subject of the next lesson.
Read this next

Permissive vs restrictive policies

The exact combination semantics, from the source — the rule behind "permission OR self, AND tenant."

postgresql.org — Row Security Policies (permissive vs restrictive)
postgresql.org — CREATE POLICY · in-repo migrations/bob/1421,1422, migrations/auth/1001

Check yourself (from memory)

Q1. What does rls_users_select_location require to show a users row?

It joins granted_permissionsuser_access_paths on app.user_id — the DB version of Lesson 8's check.

Q2. How does Postgres combine permissive and restrictive policies?

Permissive = OR-ed (any reason grants); restrictive = AND-ed (all conditions must hold). Here: permission-or-self AND tenant.

Q3. Why does the same location-permission rule live in both the service layer and RLS?

Same rule, enforced twice: a forgotten app check is caught by RLS, a bad policy is narrowed by the app check. Independent layers.
Recall: location-aware RLS + the combination rule.
the policy + permissive/restrictive, then reveal
Sensitive tables (users, locations, user_access_paths) add policies checking permission on the row's location, not just tenant. rls_users_select_location (bob/1421): visible if granted_permissions p JOIN user_access_paths usp shows the caller (app.user_id) holds user.user.read on the row's user's location. rls_users_permission_v4 (bob/1422): self-visibility (app.user_id = user_id). Combination: a row is accessible if ≥1 PERMISSIVE passes (OR) AND all RESTRICTIVE pass (AND). So users = (has-permission-on-location OR is-self) AND tenant. Same rule as Lesson 8's app-layer check — enforced independently in the DB → defense in depth.
🎯 Interview one-liner "Can RLS do more than tenant isolation?" → "Yes — our sensitive tables have policies that join the granted-permissions view to the location paths, so a row is only visible if the caller holds the right permission on that row's location, plus a self-visibility policy. Postgres OR-s the permissive policies and AND-s a restrictive tenant policy, so it's 'permission or self, within your tenant' — the same rule the service layer checks, enforced independently."
Next: we put the whole chain together — one real request traced through all six gates, and exactly what each one catches that the others don't. The payoff lesson. Ask me if the permissive/restrictive rule needs another example.

1. Postgres RLS — permissive vs restrictive. In-repo: migrations/bob/1421_migrate.up.sql:11, migrations/bob/1422_migrate.up.sql:4, migrations/auth/1001_migrate.up.sql:544,559,608.