主題: PostgreSQL
Supabase RLS 別只點 Policy UI:把授權規則寫成可測試的 SQL
將 Supabase RLS 的啟用、最小權限、操作別 policy 與 allow/deny SQL tests 放進 migration,讓每個環境都能重播與審查。
Supabase Dashboard 的 Policy UI 很方便。它能幫我探索資料表,也能很快看見一條規則長什麼樣子。但如果下一位開發者拿到專案,或 CI 要重建資料庫,真正需要的不是一張 UI 截圖,而是一套能從 Git 重播的 SQL。
以協作待辦的 public.tasks 為例。資料表有 id、owner_id、title 和 done,登入者只能管理自己的 task,未登入請求完全不能存取。要讓這個規則可審查,migration、資料表權限、policy 和測試要一起存在。
動態迷因(展開/收合)
Dashboard 的設定,要回到 migration
可以先在 Dashboard 探索 policy,不過重要變更要回到 SQL。若遠端已經有手動修改,可以用 supabase db pull 擷取 schema 變更,再審查產生的 migration。要在本機驗證時,supabase db reset 會重建本機資料庫、重套 migrations,並依設定載入 seed data。Supabase 的 migration 文件把這些工作分成建立、比對、重播與推送,而不是把 Dashboard 當成唯一來源。
以 tasks 為例,migration 先把資料表、RLS 與 client 角色的權限放在同一處:
create table public.tasks (
id uuid primary key default gen_random_uuid(),
owner_id uuid not null references auth.users(id),
title text not null,
done boolean not null default false
);
alter table public.tasks enable row level security;
revoke all on table public.tasks from anon, authenticated;
grant select, insert, update, delete on table public.tasks to authenticated;
這個順序有兩個原因。第一,SQL migration 建表不會替你自動啟用 RLS。第二,policy 不會取代 GRANT。Supabase 會先檢查角色是否能對資料表執行指令,再檢查 RLS policy 是否允許碰到那些資料列。缺少 SELECT 權限時,即使讀取 policy 寫得正確,請求仍會先被拒絕。
先撤回 client role 的既有權限,再只加回應用程式真的需要的操作,才能維持最小權限。這是 Supabase RLS 文件要求一起檢查的兩層邊界。
anon 是未登入請求的資料庫角色
anon 指的是 Supabase 對未登入 API 請求使用的 PostgreSQL role。使用者有有效登入 session 時,請求會是 authenticated role。它不是 Supabase Auth 所說的「匿名使用者」帳號。後者雖然沒有永久身分,存取資料庫時仍會使用 authenticated role,並可由 JWT claim 區分。
因此,TO authenticated 的意思是「這條 policy 只對登入請求適用」,不是「這張表已經自動禁止所有其他角色」。每個 role 還是需要適當的資料表 GRANT。service_role 有完整資料表權限且會繞過 RLS,只能留在受信任的伺服器端,不能交給瀏覽器。
每種操作各寫一條 policy
同一位使用者的讀、寫、改、刪需要看的資料列不完全相同。將操作拆開,審查時比較容易看出規則是否擴權:
create policy "task owners read"
on public.tasks for select to authenticated
using ((select auth.uid()) = owner_id);
create policy "task owners insert"
on public.tasks for insert to authenticated
with check ((select auth.uid()) = owner_id);
create policy "task owners update"
on public.tasks for update to authenticated
using ((select auth.uid()) = owner_id)
with check ((select auth.uid()) = owner_id);
create policy "task owners delete"
on public.tasks for delete to authenticated
using ((select auth.uid()) = owner_id);
USING 檢查的是已存在的資料列。它決定讀得到哪一列,也決定 UPDATE 或 DELETE 能選中哪一列。WITH CHECK 檢查的是寫入後的新資料列。對 UPDATE 同時寫兩個條件,才可以阻止使用者修改別人的 task,也阻止他把自己的 task 改成別人的 owner_id。官方範例也提醒,Data API 的 UPDATE 需要對應的 SELECT policy,否則行為不會如預期。
測試的是「有沒有真的被拒絕」
在 supabase/tests/ 建立 SQL test 後,以 supabase test db 執行 pgTAP assertions。測試要用 transaction 包住,讓資料和 request 身分不會污染下一個案例:
begin;
select plan(3);
set local role authenticated;
set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111';
select results_eq(
$$select title from public.tasks where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'$$,
array['write migration'],
'owner reads the task'
);
set local request.jwt.claim.sub = '22222222-2222-2222-2222-222222222222';
select is_empty(
$$update public.tasks
set done = true
where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
returning id$$,
'another user updates no task'
);
select * from finish();
rollback;
這段重點不是測試框架的語法,而是每個案例都明確設定 role 與 request 身分,再驗證允許與拒絕的兩面。成功的寫入應搭配 RETURNING 檢查實際結果。拒絕的寫入則要確認沒有列被改掉,不能只因 SQL 沒丟例外就當成成功。
一次 INSERT 如果有任何一列不符合 WITH CHECK,整個 SQL 指令會因 policy violation 失敗,不會先寫入合格列、再默默跳過不合格列。這正是測試要涵蓋批次寫入的原因。它保留單一指令的原子性,也防止 client 對部分成功做出錯誤假設。
動態迷因(展開/收合)
把 UI、SQL 和 CI 接起來
一個實用的流程可以很短:
- 在 migration 中啟用 RLS,收斂 grant,並為每種操作建立 policy。
- 用
supabase db reset在本機重播完整 schema。 - 在
supabase/tests/驗證anon的拒絕、owner 的允許,以及另一個登入者的拒絕。 - 以
supabase test db跑測試,審查 migration 後再依既有流程推進遠端環境。
supabase db diff 可以協助產生變更草稿。權限設定仍要經過 code review,逐一確認每個 role 實際拿到的資料表操作權限,以及每個 policy 是否只放行需要的資料列。近期開發者社群討論 RLS 測試時,也反覆提到這個需求:policy 存在不等於行為已被證明。那則討論只用來理解採用脈絡,正式語意仍以 Supabase 與 PostgreSQL 文件為準。
我學到什麼
- 我會把資料表
GRANT和 RLS policy 分開閱讀。規則寫得再精確,也不會補上缺少的資料表權限。 - 我會把
anon視為未登入請求的 PostgreSQL role,而不是混同於 Supabase Auth 的匿名帳號。 - 我會替每種操作測試允許與拒絕,特別是批次寫入的原子性與跨使用者寫入後資料是否保持不變。
- 我會讓 Dashboard 的探索結果回到 migration 和 SQL tests,讓下一個環境可以重播同一套授權規則。
外部參考資料
- Supabase:Row Level Security
- Supabase:Database migrations
- Supabase:Testing overview
- Supabase:Local development workflow
- Reddit:How do you actually test your RLS policies before shipping?
延伸學習
- Everything you need to know about Postgres Row Level Security|POSETTE 2024:Paul Copplestone 講解 policy 的 SQL 觀念。CLI 與 Supabase workflow 仍以目前官方文件為準。