policy หนึ่งใบผูกอยู่กับตาราง เขียนถูกทุกตัวอักษร และเจ้าของนับได้หกแถว
ตารางของคลินิกมีหกแถว สามแถวเป็นของผู้เช่า clinic-a และอีกสามแถวเป็นของ clinic-b
เจ้าของตารางเปิด row-level securityrow-level security (RLS)กลไกของ PostgreSQL ที่ให้นิพจน์ตัดสินเป็นรายแถวว่าคำสั่งหนึ่งเห็นหรือแตะแถวใดได้บ้าง · เอกสารของ PostgreSQL 18 แยกมันเป็นธงสองใบใน catalog คือ relrowsecurity กับ relforcerowsecurity และการเปิดใบแรกไม่ได้ทำให้ใบที่สองเปิดตาม · สิ่งที่ probe วัดคือผลของการแยกนั้นบนตาราง t_owner ไม่ใช่ค่าของธงบนตารางที่ยกไว้แค่ใบแรก ไว้แล้ว และผูก policy ไว้หนึ่งใบ
นิพจน์ของใบนั้นเทียบ tenant_id กับค่าตั้งของ session ตรงไปตรงมา
แล้วเจ้าของตารางสั่งนับแถวของตัวเอง ค่าที่วัดได้คือ 6
หกคือทั้งตาราง จำนวนแถวที่ policy ใบนั้นคัดออกไปจึงเป็นศูนย์ บทนี้ตอบว่าทำไม แล้วเปลี่ยนมันเป็น 0 ด้วยคำสั่งเดียว ที่ไม่แตะ SQL ของ policy สักตัวอักษร
คอร์สนี้อยู่ในซีรีส์คลินิกเดียวกับ คอร์สคัดกรองผู้เรียก และ คอร์ส CORS กับ CSRF แต่หกบทแรกไม่มีแอปมาเกี่ยวข้อง มีแต่ตาราง policy และ role
ตัวเลขทุกตัวในบทนี้คุณรันเองได้ สั่ง scripts/clinic-rls/up.sh ซึ่งยก container
ชื่อ kaen-rls-pg ขึ้นที่ port 55438 แล้วสั่ง python3 scripts/clinic-rls/spike.py
เก็บกวาดด้วย docker rm -f kaen-rls-pg
ตารางหกแถว และ policy ใบเดียวที่ผูกกับมัน
หัวข้อที่มีชื่อว่า “ตารางหกแถว และ policy ใบเดียวที่ผูกกับมัน”ทั้งบทตั้งอยู่บน DDL ก้อนเดียว และก้อนนี้ไม่ถูกแก้อีกเลยจนจบบท
create table t_owner ( id bigserial primary key, tenant_id text not null, patient text not null, archived boolean not null default false);insert into t_owner (tenant_id, patient, archived) values ('clinic-a','Ann',false),('clinic-a','Ben',false),('clinic-a','Cid',true), ('clinic-b','Dan',false),('clinic-b','Eve',false),('clinic-b','Fay',true);grant select, insert, update, delete on t_owner to kaen_rls_app, kaen_rls_bypass;grant usage, select on sequence t_owner_id_seq to kaen_rls_app, kaen_rls_bypass;alter table t_owner enable row level security;create policy p on t_owner for all using (tenant_id = current_setting('app.tenant', true));บรรทัดรองสุดท้ายคือ ENABLE ROW LEVEL SECURITYALTER TABLE … ENABLE ROW LEVEL SECURITYคำสั่งที่ยกธงใบแรกให้ตาราง · ธงใบนี้ไม่ครอบเจ้าของตาราง ทรงที่เจ้าของอ่านตาราง t_owner หกแถวของตัวเองใต้ ENABLE ล้วน วัดได้ 6 แถว (ownerEnableNoGuc) คือทั้งตาราง จำนวนแถวที่ policy คัดออกไปจึงเป็นศูนย์ ซึ่งเป็นคำสั่งที่คนส่วนใหญ่จำได้ว่า เป็นสวิตช์เปิดกลไกนี้ บรรทัดสุดท้ายคือ CREATE POLICYCREATE POLICYคำสั่งที่ผูกนิพจน์หนึ่งเข้ากับตารางหนึ่ง · ตาราง t_app ของ probe มี policy ผูกอยู่ 1 ใบ (appPolicyCount) และเขียนถูกทุกตัวอักษร แต่จำนวน policy ที่ผูกอยู่ไม่ใช่หลักฐานว่ามันเคยถูกเรียกใช้ ซึ่งผูกนิพจน์หนึ่งเข้ากับตาราง
for all แปลว่าใบนี้ครอบทั้ง SELECT INSERT UPDATE และ DELETE
ส่วน current_setting('app.tenant', true) อ่านค่าตั้งของ session ที่ชื่อ app.tenant
อาร์กิวเมนต์ตัวที่สองสั่งไม่ให้ยกข้อผิดพลาด เมื่อยังไม่มีใครตั้งค่านั้นใน session นี้
บทที่ 2 วัดว่าค่าที่ได้ในกรณีนั้นคืออะไร
probe อ่านผลกลับมาด้วยการต่อ 'KAEN|<ชื่อคีย์>|' ไว้หน้าค่าทุกบรรทัด
ชื่อคีย์ที่คุณเห็นในทุก fence หลังจากนี้ คือชื่อที่ผลของ spike.py ใช้เก็บค่านั้นไว้
ตัวเลขในบทนี้จึงอ้างอิงเป็นคู่ของทรงกับคีย์เสมอ
เจ้าของตารางนับแถวของตัวเอง แล้วได้ทั้งตาราง
หัวข้อที่มีชื่อว่า “เจ้าของตารางนับแถวของตัวเอง แล้วได้ทั้งตาราง”policy ผูกอยู่แล้ว RLS เปิดอยู่แล้ว และค่าที่ได้เท่ากับจำนวนแถวทั้งตาราง
select 'KAEN|ownerEnableNoGuc|'||count(*) from t_owner where true;ownerEnableNoGuc = 6
ค่านี้ไม่ใช่ความผิดพลาดของนิพจน์ ถ้านิพจน์ถูกเรียกใช้จริง มันจะเทียบ tenant_id
ของทุกแถวกับค่าที่ยังไม่มีใครตั้ง แล้วไม่มีแถวไหนผ่าน หัวข้อถัดไปวัดข้อนี้ออกมาเป็นตัวเลข
สิ่งที่เกิดขึ้นคือนิพจน์ไม่เคยถูกเรียกใช้เลย เจ้าของตารางไม่ได้อยู่ใต้ policy ของตัวเอง
ด้วยคำสั่ง ENABLE เพียงคำสั่งเดียว
เติม FORCE เข้าไป โดยไม่แตะ SQL ของ policy
หัวข้อที่มีชื่อว่า “เติม FORCE เข้าไป โดยไม่แตะ SQL ของ policy”คำสั่งที่เพิ่มเข้ามาคือ ALTER TABLE ไม่ใช่ CREATE POLICY และตัวเลขขยับสี่ครั้งในลำดับเดียว
alter table t_owner force row level security;select 'KAEN|ownerForceNoGuc|'||count(*) from t_owner where true;set app.tenant = 'clinic-a';select 'KAEN|ownerForceGucA|'||count(*) from t_owner where true;set app.tenant = 'clinic-b';select 'KAEN|ownerForceGucB|'||count(*) from t_owner where true;alter table t_owner no force row level security;select 'KAEN|ownerNoForceGucB|'||count(*) from t_owner where true;| สิ่งที่เพิ่งสั่งไปก่อนหน้า | คีย์ | แถวที่นับได้ |
|---|---|---|
force row level security · ยังไม่ตั้ง app.tenant | ownerForceNoGuc | 0 |
set app.tenant = 'clinic-a' | ownerForceGucA | 3 |
set app.tenant = 'clinic-b' | ownerForceGucB | 3 |
no force row level security · app.tenant ยังเป็น clinic-b | ownerNoForceGucB | 6 |
แถวแรกปิดคำถามของหัวข้อก่อนหน้า นิพจน์ใบเดิมในตารางใบเดิมให้ 0 เมื่อมันถูกเรียกใช้จริง ส่วนที่ให้ 6 ก่อนหน้านั้นคือทรงที่มันไม่ถูกเรียกใช้
FORCE ROW LEVEL SECURITYALTER TABLE … FORCE ROW LEVEL SECURITYคำสั่งที่ยกธงใบที่สอง ซึ่งพาเจ้าของตารางเข้ามาอยู่ใต้ policy ด้วย · ทุกค่าต่อจากนี้วัดบนตาราง t_owner ที่เจ้าของอ่านเอง · เติมแล้วเจ้าของที่ไม่ได้ตั้ง GUC ได้ 0 แถว ตั้งเป็นผู้เช่า a ได้ 3 แถว ตั้งเป็น b ได้ 3 แถว และสั่ง NO FORCE ทั้งที่ GUC ยังตั้งอยู่ กลับไปได้ 6 แถว โดย SQL ของ policy ไม่เปลี่ยนสักตัวอักษรในทุกทรง คือคำสั่งที่พาเจ้าของตารางเข้ามาอยู่ใต้ policy ด้วย สองแถวกลางแสดงว่านิพจน์ทำงานตามที่เขียนไว้ ผู้เช่าไหนถูกตั้งไว้ ก็เห็นสามแถวของผู้เช่านั้น
แถวสุดท้ายคือแถวที่อ่านง่ายที่สุดในตาราง app.tenant ยังเป็น clinic-b อยู่
ไม่มีใครแตะ policy และค่ากลับไปเป็น 6 สิ่งที่ยกขึ้นแล้วลดลงจึงไม่ใช่นิพจน์
แต่เป็นสถานะของตาราง
ธงสองใบ ที่ไม่ได้เปิดตามกัน
หัวข้อที่มีชื่อว่า “ธงสองใบ ที่ไม่ได้เปิดตามกัน”
ENABLEกับFORCEยกธงคนละใบใน catalog และใบแรกไม่ได้ยกใบที่สองให้
เอกสารของ PostgreSQL 18 อธิบายไว้ว่าตารางหนึ่งถือธงสองใบใน pg_class
คือ relrowsecurity ที่ ENABLE ยก และ relforcerowsecurity ที่ FORCE ยก
ข้อนี้เป็นข้ออ้างเรื่องกลไก จึงอ้างเอกสาร ไม่ได้อ้าง probe
เอกสารหน้าเดียวกันระบุว่าเจ้าของตารางถูกยกเว้นจาก policy ตามค่าปริยาย และใบที่สองคือใบที่ถอนการยกเว้นนั้น สี่แถวในตารางข้างบนคือค่าที่วัดได้ของกฎข้อนี้
การแยกสองข้อนี้ออกจากกันสำคัญกว่าที่เห็น ตัวเลข 6 กับ 0 เป็นสิ่งที่ probe วัด ส่วนคำอธิบายว่าธงใบไหนทำอะไร เป็นสิ่งที่เอกสารกำหนด บทนี้ไม่ได้พิสูจน์ข้อหลัง มันเพียงแสดงผลที่สอดคล้องกับข้อหลัง
ตารางของ superuser ธงเป็น true ทั้งสองใบ แล้วยังได้หก
หัวข้อที่มีชื่อว่า “ตารางของ superuser ธงเป็น true ทั้งสองใบ แล้วยังได้หก”ทรงนี้ยกธงครบทั้งสองใบ อ่านค่าธงออกมาจาก catalog จริง แล้วจำนวนแถวยังไม่ขยับ
ทรงที่สองสร้างตาราง t_super ด้วย DDL ก้อนเดียวกับข้างบนทุกบรรทัด
แล้วสั่ง force row level security ต่อท้าย ต่างกันที่ผู้สั่งเป็น postgres
ซึ่งเป็น superuser ตารางใบนี้จึงมีทั้ง policy ทั้ง ENABLE และทั้ง FORCE
select 'KAEN|superIsSuper|'||rolsuper from pg_roles where rolname = current_user;select 'KAEN|superRlsEnabled|'||relrowsecurity from pg_class where oid='t_super'::regclass;select 'KAEN|superRlsForced|'||relforcerowsecurity from pg_class where oid='t_super'::regclass;select 'KAEN|superOwnerForceNoGuc|'||count(*) from t_super where true;set app.tenant = 'clinic-a';select 'KAEN|superOwnerForceGucA|'||count(*) from t_super where true;| คีย์ | ค่าที่วัดได้ |
|---|---|
superIsSuper | true |
superRlsEnabled | true |
superRlsForced | true |
superOwnerForceNoGuc | 6 |
superOwnerForceGucA | 6 |
สามแถวบนคือธงที่อ่านออกมาจาก catalog จริง ไม่ใช่สิ่งที่บทนี้เดาจากคำสั่งที่สั่งไป
สองแถวล่างคือจำนวนแถวที่ผู้อ่านคนนั้นได้กลับไป ทั้งตอนไม่ตั้งค่าและตอนตั้งเป็น clinic-a
ทรงนี้คือทรงที่หลอกได้มากที่สุดในบท ตารางมี policy มีธงครบทั้งสองใบ และค่าที่อ่านออกมาจาก catalog ยืนยันว่าครบจริง แต่ตัวเลขยังเป็นหก superuser ข้าม policy โดยไม่มีเงื่อนไข ซึ่งเป็นข้อที่เอกสารกำหนด ไม่ใช่ข้อที่ธงสองใบนี้บอก
role ที่ถือธง BYPASSRLS
หัวข้อที่มีชื่อว่า “role ที่ถือธง BYPASSRLS”ทางที่สาม ไม่ได้อยู่ที่ตาราง แต่อยู่ที่ตัว role ของผู้อ่าน
ทรงที่สามสร้างตาราง t_app ด้วย DDL ก้อนเดิมและ policy ใบเดิม
probe นับ policy ที่ผูกอยู่กับตารางใบนี้จาก pg_policy ได้ appPolicyCount = 1
ผู้อ่านในทรงนี้คือ role คนละใบกับเจ้าของตาราง และถือธง BYPASSRLSBYPASSRLSธงของ role ที่ให้ข้าม policy ทุกใบ · role ที่มีธงนี้อ่านตาราง t_app ซึ่งเปิด RLS แล้วและมี policy ผูกอยู่หนึ่งใบ ได้ 6 แถวทั้งตอนตั้ง GUC และตอนไม่ตั้ง ขณะที่ role ที่ไม่มีธงบนตารางเดียวกันได้ 3 แถวและ 0 แถว · superuser ข้ามการตรวจสิทธิ์ทุกชนิดอยู่แล้ว ซึ่งเป็นคนละเงื่อนไขกับธงใบนี้ ไม่ใช่การไม่มีธง · probe จึงยืนยันก่อนวัดว่าทุก role ที่มันสร้างไม่ใช่ superuser (rolesAreSuperuser = 0)
select 'KAEN|bypassNoGuc|'||count(*) from t_app where true;set app.tenant = 'clinic-a';select 'KAEN|bypassGucA|'||count(*) from t_app where true;bypassNoGuc = 6 และ bypassGucA = 6
สองค่านี้เท่ากัน ซึ่งเป็นสิ่งที่ต้องอ่านให้ครบ การตั้ง app.tenant เป็น clinic-a
ไม่ได้ทำให้ตัวเลขขยับแม้แต่แถวเดียว ค่าตั้งที่ policy อ่านจึงไม่มีผลกับ role ใบนี้เลย
เพราะนิพจน์ไม่ได้ถูกเรียกใช้ตั้งแต่ต้น
เทียบกับทรงแรก นิพจน์รูปเดียวกันที่ถูกเรียกใช้จริง ให้ ownerForceGucA = 3
เมื่อ app.tenant เป็น clinic-a ธงใบนี้จึงไม่ได้เปลี่ยนผลของนิพจน์
มันเอานิพจน์ออกจากทางเดินของคำสั่ง
BYPASSRLS ผูกกับ role ไม่ได้ผูกกับตาราง คนที่ตรวจตารางของตัวเองว่า
เปิดครบทั้งสองใบแล้ว ยังตอบไม่ได้ว่ามีใครอ่านตารางนั้นแบบข้าม policy อยู่หรือไม่
superIsSuper เป็น true และ bypassNoGuc เป็น 6 เป็นสองข้อที่แยกกัน
ข้อแรกคือ superuser ซึ่งเอกสารจัดเป็นเงื่อนไขคนละข้อกับธงนี้ ข้อที่สองคือ role
ที่ probe ยืนยันก่อนวัดแล้วว่าไม่ใช่ superuser และถือธงนี้ติดตัวมา
ข้อเดียวที่ทำให้ตัวเลขที่เหลือทั้งคอร์สมีความหมาย
หัวข้อที่มีชื่อว่า “ข้อเดียวที่ทำให้ตัวเลขที่เหลือทั้งคอร์สมีความหมาย”ถ้า role ที่ probe ใช้วัดเป็น superuser มันจะพิมพ์ตัวเลขออกมาครบ โดยไม่ได้วัดอะไรเลย
หัวข้อก่อนหน้าเพิ่งแสดงว่า superuser ข้าม policy โดยไม่มีเงื่อนไข ข้อนั้นทำให้ probe ที่ต่อฐานข้อมูลด้วย role ผิดใบ กลายเป็น probe ที่รายงานหกแถวทุกทรง แล้วอ่านเหมือนการค้นพบ ทั้งที่มันคือการไม่ได้วัด
create role kaen_rls_owner login nosuperuser nocreatedb nocreaterole;create role kaen_rls_app login nosuperuser nocreatedb nocreaterole;create role kaen_rls_bypass login nosuperuser nocreatedb nocreaterole bypassrls;spike.py ตรวจ role ทั้งสามใบทันทีที่สร้างเสร็จ ก่อนที่ทรงไหนจะได้วัดสักทรง
ถ้ามีใบไหนเป็น superuser มันหยุดด้วยรหัสออก 2 ซึ่งแปลว่าวัดไม่ได้ ไม่ใช่ข้ออ้างผิด
จำนวนที่นับได้ถูกเก็บเป็นค่าที่วัดได้ตัวหนึ่งด้วย คือ rolesAreSuperuser = 0
ธง bypassrls บนบรรทัดที่สามคือธงที่ตั้งใจใส่ให้ role ใบเดียว
และ probe ยืนยันก่อนวัดด้วยว่าอีกสองใบไม่มีธงนี้ติดมา
สามทาง ที่จบลงที่หกแถวเหมือนกัน
หัวข้อที่มีชื่อว่า “สามทาง ที่จบลงที่หกแถวเหมือนกัน”ทางแรกอยู่ที่ตาราง ทางที่สองอยู่ที่ชนิดของ role และทางที่สามอยู่ที่ธงบน role
flowchart TB
A1["ทรงที่ 1 · เจ้าของตาราง อ่าน t_owner ของตัวเอง"] --> A2{"สั่ง FORCE ให้ t_owner แล้วหรือยัง"}
A2 -->|"ยัง · มีแค่ ENABLE"| A3["6 แถว · ownerEnableNoGuc และ ownerNoForceGucB"]
A2 -->|"สั่งแล้ว"| A4{"app.tenant ถูกตั้งไว้หรือยัง"}
A4 -->|"ยังไม่ตั้ง"| A5["0 แถว · ownerForceNoGuc"]
A4 -->|"clinic-a หรือ clinic-b"| A6["3 แถว · ownerForceGucA และ ownerForceGucB"]
B1["ทรงที่ 2 · superuser อ่าน t_super ของตัวเอง · ธงใน catalog เป็น true ทั้งสองใบ"] --> B2["6 แถว ทั้งตอนตั้งและไม่ตั้ง app.tenant"]
C1["ทรงที่ 3 · role ที่ถือธง BYPASSRLS อ่าน t_app"] --> C2["6 แถว ทั้งตอนตั้งและไม่ตั้ง app.tenant"]
คำบรรยายภาพ: สามทรงที่บทนี้วัด ทุกทรงมี policy ผูกอยู่กับตารางและเปิด RLS ไว้แล้ว ทรงที่หนึ่งเป็นทางเดียวที่นิพจน์ถูกเรียกใช้ และเป็นทางเดียวที่ตัวเลขขยับ
ปลายทางสามใบที่เขียนว่าหกแถว มาจากเงื่อนไขคนละข้อกันทั้งสามใบ
ใบหนึ่งแก้ด้วย ALTER TABLE ใบที่สองแก้ด้วยการไม่ต่อฐานข้อมูลด้วย superuser
และใบที่สามแก้ด้วยการถอนธงออกจาก role
ที่วัดได้ กับที่เอกสารกำหนด
หัวข้อที่มีชื่อว่า “ที่วัดได้ กับที่เอกสารกำหนด”ตัวเลขทั้งบทมาจาก SQL ที่รันจริง ส่วนคำอธิบายว่าธงใบไหนทำอะไร มาจากเอกสาร
spike.py รันชุดข้ออ้างทั้งชุดสามรอบต่อการเรียกหนึ่งครั้ง แล้วเทียบผลทีละคีย์
รอบที่รันเมื่อ 2026-09-06 ได้ 52 ข้ออ้างตรงทั้งหมด 98 ค่าที่วัดได้
และ 0 ค่าที่ขยับระหว่างรอบ
สิ่งที่บทนี้ ไม่ได้ พิสูจน์คือกลไกข้างใน บทนี้ไม่ได้แสดงว่าตัวตัดสิน อ่านธงใบไหนก่อนใบไหน ข้ออ้างแบบนั้นอ้างเอกสารของ PostgreSQL 18 และเอกสารเป็นคนละหลักฐานกับ probe
ผลทั้งหมดในบทนี้วัดบน PostgreSQL 18.6 บน aarch64-unknown-linux-musl
ใน container ของคอร์สนี้เอง major อื่นเป็นคนละการวัด และบทนี้ไม่ได้อ้างถึงมัน
หน่วยของหลักฐานในบทนี้มีสามหน่วย คือจำนวนแถวที่ผู้อ่านนับได้
ธงบูลีนที่อ่านออกมาจาก pg_class กับ pg_roles และจำนวน policy ที่ผูกกับตาราง
ทั้งคอร์สไม่มีหน่วยเวลาสักตัว
คำถามที่บทนี้ทิ้งไว้
หัวข้อที่มีชื่อว่า “คำถามที่บทนี้ทิ้งไว้”รู้แล้วว่าอะไรทำให้ policy ถูกเรียกใช้ ยังไม่รู้ว่าค่าที่มันอ่าน อยู่ได้นานแค่ไหน
ทรงที่หนึ่งใต้ FORCE ให้ 0 เมื่อไม่ตั้ง app.tenant และให้ 3
เมื่อตั้งเป็นผู้เช่าใบใดใบหนึ่ง สองค่านั้นวัดได้แล้ว
แต่ยังไม่มีข้อไหนบอกว่าค่าที่ตั้งไปนั้นถูกเก็บไว้ที่ไหน
ถ้ามันเป็นค่าของ session คำถามถัดไปคือ session หนึ่งกินเวลาถึงคำขอกี่ใบ และคำสั่งที่ล้างค่านั้นทิ้ง คืนสถานะกลับไปเป็นแบบที่ไม่เคยตั้งเลยจริงหรือไม่
บทที่ 2 ตั้งค่านั้นด้วยสองรูปที่ต่างกัน แล้ววัดว่าหลัง commit แต่ละรูปเหลืออะไร คำตอบของสองรูปนั้นไม่เท่ากัน และความต่างของมันเดินไปโผล่อีกครั้งที่บทที่ 7
-
PostgreSQL 18 — Row Security Policies (เข้าถึง 2026-09-06) — ที่มาของกฎว่าเจ้าของตารางถูกยกเว้นจาก policy ตามค่าปริยาย และ superuser ข้าม policy โดยไม่มีเงื่อนไข
-
PostgreSQL 18 —
ALTER TABLE(เข้าถึง 2026-09-06) — ความหมายของENABLEFORCEและNO FORCE ROW LEVEL SECURITY -
PostgreSQL 18 —
CREATE ROLE(เข้าถึง 2026-09-06) — ความหมายของธงBYPASSRLSบน role -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/clinic-rls/spike.pyซึ่งสร้างฐานข้อมูลกับ role ขึ้นใหม่ทุกรอบ แล้วรื้อทิ้งเมื่อจบ · ต้องสั่งscripts/clinic-rls/up.shก่อน ซึ่งยก container ของคอร์สนี้เองขึ้นที่ port 55438
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3เจ้าของตารางสั่ง `enable row level security` ผูก policy ที่เขียนถูกไว้หนึ่งใบ แล้วนับแถวของตารางหกแถวของตัวเอง ได้เท่าไร