RLS Patterns and Pitfalls
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”RLS ในโลกจริงส่วนใหญ่คือการผสม pattern ไม่กี่แบบเข้าด้วยกัน — owner-only, public-read-owner-write, role-based — และ bug ของ RLS ในโลกจริงส่วนใหญ่ก็มาจากการลืม policy ของ operation ใด operation หนึ่ง หรือลืม index column ที่ policy ใช้กรอง
Owner-only rows
หัวข้อที่มีชื่อว่า “Owner-only rows”pattern จากบทที่แล้ว row จะมองเห็นหรือแก้ได้เฉพาะ user ที่เป็นเจ้าของเท่านั้น โดยใช้ auth.uid() = user_id ทั้งใน using และ with check
create policy "Owners can update their own rows"on public.todosfor updateusing (auth.uid() = user_id)with check (auth.uid() = user_id);Public read, owner-only write
หัวข้อที่มีชื่อว่า “Public read, owner-only write”รูปแบบที่พบบ่อยมากสำหรับ content แบบ blog post หรือ comment ใครก็อ่านได้ แต่แก้หรือลบได้เฉพาะเจ้าของ row เท่านั้น แบบนี้ต้องใช้ policy สองอันแยกกัน อันละทิศทาง เพราะ using (true) ของ select ไม่ได้บอกอะไรเลยว่าใครเขียนได้บ้าง
-- Anyone (including anon) can readcreate policy "Anyone can read posts"on public.postsfor selectusing (true);
-- Only the owner can update their own postcreate policy "Owners can update their posts"on public.postsfor updateusing (auth.uid() = user_id)with check (auth.uid() = user_id);Role-based access
หัวข้อที่มีชื่อว่า “Role-based access”บางครั้งสิทธิ์การเข้าถึงขึ้นกับ role ไม่ใช่ความเป็นเจ้าของ เช่น admin ที่ moderate content ของทุกคนได้ วิธีที่นิยมคือใช้ table user_roles แล้ว join เข้าไปใน policy condition (custom claim ที่ฝังตรงใน JWT ก็เป็นอีกทางเลือกหนึ่ง แต่ table แบบ join จัดการง่ายกว่าเพราะไม่ต้องออก token ใหม่ทุกครั้ง)
create policy "Admins can delete any post"on public.postsfor deleteusing ( exists ( select 1 from public.user_roles where user_roles.user_id = auth.uid() and user_roles.role = 'admin' ));Pitfall: policy อันเดียวไม่ครอบคลุมทุก operation
หัวข้อที่มีชื่อว่า “Pitfall: policy อันเดียวไม่ครอบคลุมทุก operation”RLS ถูกประเมิน แยกตาม operation — select, insert, update, delete แต่ละอันเช็คกับ policy ของตัวเองแยกกันโดยอิสระ table หนึ่งใบจึงมี policy select ที่ใช้งานได้ปกติ แต่ไม่มี policy insert เลยก็ได้ง่าย ๆ ผลคือ อ่านได้ปกติ แต่ทุกการเขียนจะ fail แบบเงียบ ๆ ด้วย permission error ซึ่งดูเหมือน bug ของโค้ดฝั่งแอป ทั้งที่จริง ๆ แค่ลืม policy ก่อน deploy table ควรเช็ก operation ที่แอปต้องใช้กับ table นั้นทีละอัน แล้วให้แน่ใจว่ามี policy ครบทุกตัว
Pitfall เรื่อง performance: column ที่ policy ใช้กรองแต่ไม่มี index
หัวข้อที่มีชื่อว่า “Pitfall เรื่อง performance: column ที่ policy ใช้กรองแต่ไม่มี index”condition ของ policy รันกับทุก row ที่ Postgres พิจารณาใน query เหมือนกับ where clause เป๊ะ ถ้า column ที่ policy ใช้กรอง — ส่วนใหญ่คือ user_id ในตัวอย่างข้างบน — ไม่มี index query ที่ดูเรียบง่ายอาจกลายเป็น full sequential scan ทันทีที่ table มีข้อมูลจริงเยอะขึ้น ให้ index column ที่ RLS policy อ้างอิงเหมือนที่ทำกับ column ที่ถูก filter บ่อย ๆ ทั่วไป
create index on public.posts (user_id);อีกจุดที่ควรรู้ไว้คือการห่อ auth.uid() ด้วย select ภายใน policy condition จะทำให้ Postgres ประเมินแค่ครั้งเดียวต่อ query แทนที่จะประเมินทุก row ซึ่งมีผลชัดเจนเมื่อ table มี row เยอะขึ้น
create policy "Owners can update their posts"on public.postsfor updateusing ((select auth.uid()) = user_id);Test policy ก่อน ship
หัวข้อที่มีชื่อว่า “Test policy ก่อน ship”อย่าคิดเอาเองว่า policy ทำงานถูกต้อง ให้ test ในฐานะ user ที่ตั้งใจจะจำกัดสิทธิ์ table editor ของ Supabase Studio มี feature impersonation ไว้สำหรับเรื่องนี้โดยเฉพาะ หรือจะทำแบบเดียวกันตรง ๆ ใน SQL session ก็ได้ด้วยการสลับ role แล้วตั้ง claim ของ JWT ให้เหมือน request จริง
set role authenticated;set request.jwt.claims = '{"sub": "11111111-1111-1111-1111-111111111111", "role": "authenticated"}';
select * from public.posts; -- now runs exactly as that user would see it
reset role;flowchart TB
subgraph missing["Table with only a SELECT policy"]
m1["select policy: present"] --> m2["Reads work"]
m3["insert policy: missing"] --> m4["Writes silently blocked"]
end
subgraph covered["Table with all four policies"]
c1["select / insert / update / delete: all present"] --> c2["Reads and writes both work as intended"]
end