ข้าม​ไป​ยัง​เนื้อหา

policy ใบ​เดียว​ที่​ระบุ FOR SELECT · UPDATE ได้ 00000 และ​แก้​ไป​ศูนย์​แถว

บท​ที่ 3 วัด​การ​เขียน​ข้าม​ผู้​เช่า​ที่​ถูก​ปฏิเสธ และ​การ​ปฏิเสธ​นั้น​ส่ง​รหัส​ออก​มา​ให้​เห็น คน​ที่​อ่าน log เจอ​รหัส​นั้น ก็​รู้ทัน​ที​ว่า​มี​คำ​สั่ง​ที่​ไม่​ผ่าน

บท​นี้​เปลี่ยน policy ไป​หนึ่ง​คำ แล้ว​การ​ปฏิเสธ​เปลี่ยน​เสียง

update ที่​ตั้งใจ​จะ​แก้​สาม​แถว กลับ​มา​ด้วย​รหัส 00000 และ 0 แถว​ที่​ถูก​กระทำ ซึ่ง​เป็น​ค่า​เดียว​กับ​ที่​คำ​สั่ง​ซึ่ง​ไม่มี​อะไร​ให้​ทำ กลับ​มา​ด้วย

📦 clinic — ซี​รีส์​เดิม แต่​หกบท​แรก​เป็น SQL ล้วน

คอร์ส​นี้​อยู่​ใน​ซี​รีส์​คลินิก​เดียว​กับ คอร์ส​คัด​กรอง​ผู้​เรียก และ คอร์ส 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 ก้อน​นี้​ต่าง​จาก​ก้อน​ของ​บท​ที่ 1 ที่​ชื่อ​ตาราง ชื่อ policy และ​อีก​คำ​เดียว​บน​บรรทัด​สุดท้าย

ทรง​ที่ ⑥ · ตาราง t_selonly · สั่ง​โดย role ที่​เป็น​เจ้าของ​ตาราง
create table t_selonly (
id bigserial primary key,
tenant_id text not null,
patient text not null,
archived boolean not null default false);
insert into t_selonly (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_selonly to kaen_rls_app, kaen_rls_bypass;
grant usage, select on sequence t_selonly_id_seq to kaen_rls_app, kaen_rls_bypass;
alter table t_selonly enable row level security;
create policy p_sel on t_selonly for select using (tenant_id = current_setting('app.tenant', true));

บรรทัด​สุดท้าย​คือ CREATE POLICYCREATE POLICYคำ​สั่ง​ที่​ผูก​นิพจน์​หนึ่ง​เข้า​กับ​ตาราง​หนึ่ง · ตาราง t_app ของ probe มี policy ผูก​อยู่ 1 ใบ (appPolicyCount) และ​เขียน​ถูก​ทุก​ตัว​อักษร แต่​จำนวน policy ที่​ผูก​อยู่​ไม่ใช่​หลัก​ฐาน​ว่า​มัน​เคย​ถูก​เรียก​ใช้ ที่​ระบุ for select แทน for all นิพจน์​ใน USINGUSING (…)นิพจน์​ที่ policy ใช้​คัด​ว่า​แถว​ใด​มอง​เห็น​หรือ​แตะ​ได้ · เมื่อ policy ไม่มี WITH CHECK นิพจน์​นี้​ทำ​หน้าที่​ตรวจ​แถว​หลัง​การ​เขียน​ให้​เอง​ด้วย · UPDATE ที่​ย้าย​แถว​ข้าม​ผู้​เช่า​บน​ตาราง t_update ใต้ USING ล้วน ได้ 42501 และ​ยอด​ของ​สอง​ผู้​เช่า​ค้าง​ที่ 3 กับ 3 เป็น​นิพจน์​เดียว​กับ​ที่​บท​ที่ 1 ใช้​ทุก​ตัว​อักษร สิ่ง​ที่​เปลี่ยน​คือ​ขอบเขต​ของ​ใบ​นี้ ไม่ใช่​เนื้อ​ใน​ของ​มัน

ผู้​อ่าน​ใน​ทรง​นี้​เป็น role kaen_rls_app ซึ่ง​ไม่ใช่​เจ้าของ​ตาราง ตาราง​จึง​ไม่​ต้องการ force row level security เพื่อ​ให้ policy ทำงาน grant สี่​สิทธิ์​บน​บรรทัด​ที่​เก้า​ยืนยัน​ว่า role ใบ​นี้​มี​สิทธิ์​สั่ง​ครบ​ทั้ง​สี่​คำ​สั่ง

สาม​คำ​สั่ง​เขียน ใต้ policy ที่​ไม่​ได้​พูด​ถึง​มัน​สัก​คำ

หัวข้อ​ที่​มีชื่อ​ว่า “สาม​คำ​สั่ง​เขียน ใต้ policy ที่​ไม่​ได้​พูด​ถึง​มัน​สัก​คำ”

คำ​สั่ง​อ่าน​ได้​สาม​แถว แล้ว​คำ​สั่ง​เขียน​สาม​คำ​สั่ง​กลับ​มา​ด้วย​ค่าที่​ไม่​เหมือน​กัน

ทรง​ที่ ⑥ · session ของ kaen_rls_app · ตั้ง app.tenant เป็น clinic-a
set app.tenant = 'clinic-a';
select 'KAEN|selOnlyVisible|'||count(*) from t_selonly where true;
select 'KAEN|selOnlyUpdate|'||public.kaen_try('update t_selonly set patient = patient || ''!'' where tenant_id=''clinic-a''');
select 'KAEN|selOnlyDelete|'||public.kaen_try('delete from t_selonly where tenant_id=''clinic-a''');
select 'KAEN|selOnlyInsert|'||public.kaen_try('insert into t_selonly (tenant_id, patient) values (''clinic-a'',''Gil'')');

public.kaen_try เป็น​ตัว​ช่วย​ของ probe ตัว​เดียว มัน​สั่ง​คำ​สั่ง​ที่​รับ​มา แล้ว​คืน​สตริง​สอง​ช่อง ช่อง​แรก​เป็น​รหัส SQLSTATESQLSTATEรหัส​ห้า​หลัก​ที่ PostgreSQL ให้​กับ​ผล​ของ​คำ​สั่ง​หนึ่ง · คอร์ส​นี้​ยก​เฉพาะ​รหัส ไม่​ยก​ข้อความ​ผิด​พลาด เพราะ​ข้อความ​เป็น​ร้อย​แก้ว​ที่​เปลี่ยน​ข้าม major ส่วน​รหัส​ไม่​เปลี่ยน · 00000 แปล​ว่า​คำ​สั่ง​สำเร็จ ซึ่ง​เกิด​คู่​กับ​จำนวน​แถว​ที่​ถูก​กระทำ​เท่าใด​ก็ได้ และ​ช่อง​ที่​สอง​เป็น​จำนวน​แถว​ที่​ถูก​กระทำ เส้น​ทาง​ที่​ยก​ข้อ​ผิด​พลาด​คืน -1 ใน​ช่อง​ที่​สอง ซึ่ง​เป็น​หมุด​ที่​แปล​ว่า​ไม่มี​จำนวน​แถว​ให้​รายงาน

คีย์คำ​สั่ง​ที่ probe สั่งค่าที่​วัด​ได้
selOnlyVisibleselect count(*)3
selOnlyUpdateupdate แถว​ของ clinic-a00000 0
selOnlyDeletedelete แถว​ของ clinic-a00000 0
selOnlyInsertinsert แถว​ของ clinic-a42501 -1

สาม​แถว​ที่ selOnlyVisible นับ​ได้ คือ​แถว​ที่ policy ใบ​นี้​ยอม​ให้​เห็น คำ​สั่ง update กับ delete ชี้​ไป​ที่​แถว​ชุด​เดียวกัน​ด้วย​เงื่อนไข​ตัว​เดียวกัน แล้ว​ทั้ง​คู่​กลับ​มา​ด้วย 00000 และ 0

ส่วน selOnlyInsert กลับ​มา​ด้วย​รหัส 42501 insufficient_privilegeSQLSTATE 42501 (insufficient_privilege)รหัส​ที่ RLS ใช้​ปฏิเสธ​การ​เขียน​แบบ​ส่ง​เสียง​ออก​มา · วัด​ได้​ใน​ห้า​ทรง คือ UPDATE บน​ตาราง t_update ที่​ย้าย​แถว​ข้าม​ผู้​เช่า ทั้ง​ใต้ USING ล้วน​และ​ใต้ WITH CHECK (true) · INSERT ข้าม​ผู้​เช่า​บน​ตาราง t_insert ใต้ USING ล้วน · ทรง​เดียวกัน​ที่​ผ่าน​แล้ว​แต่​เติม RETURNING เข้าไป · และ INSERT บน​ตาราง t_selonly ใต้ policy ที่​ระบุ FOR SELECT อย่าง​เดียว · ตรง​ข้าม​กับ UPDATE และ DELETE บน​ตาราง​ใบ​หลัง​นั้น ซึ่ง​คืน 00000 และ 0 แถว​โดย​ไม่​ส่ง​เสียง คำ​สั่ง​เดียว​ที่​ส่ง​เสียง​ออก​มา คือ​คำ​สั่ง​ที่​ไม่มี​แถว​เดิม​ให้​อ้าง​ถึง​เลย

ตาราง​หลัง​จบ · หก​แถว และ​ไม่มี​เครื่องหมาย​อัศเจรีย์​สัก​ตัว

หัวข้อ​ที่​มีชื่อ​ว่า “ตาราง​หลัง​จบ · หก​แถว และ​ไม่มี​เครื่องหมาย​อัศเจรีย์​สัก​ตัว”

ศูนย์​แถว​ที่ update รายงาน เป็น​รายงาน​ที่​ตรง และ​เจ้าของ​ตาราง​ยืนยัน​มัน​ได้​จาก​อีก​ฝั่ง

คำ​สั่ง update ใน​ทรง​ข้าง​บน​ต่อ​เครื่องหมาย ! ท้าย​ชื่อ​ของ​ทุก​แถว​ที่​มัน​แก้​ได้ ร่อง​รอย​ของ​มัน​จึง​นับ​ได้ และ delete ที่​ลบ​สำเร็จ​จะ​ทำให้​จำนวน​แถว​ทั้ง​ตาราง​ลด​ลง probe เปลี่ยน​ไป​อ่าน​ด้วย role ที่​เป็น​เจ้าของ​ตาราง ซึ่ง​บท​ที่ 1 วัด​แล้ว​ว่า​อยู่​นอก policy ของ​ตัว​เอง

ทรง​ที่ ⑥ · session ของ kaen_rls_owner · หลัง​คำ​สั่ง​ทั้ง​สาม​จบ​ไป​แล้ว
select 'KAEN|selOnlyRowsAfter|'||count(*) from t_selonly where true;
select 'KAEN|selOnlyBangs|'||count(*) from t_selonly where patient like '%!';

selOnlyRowsAfter = 6 และ selOnlyBangs = 0

หก​คือ​จำนวน​แถว​ตั้ง​ต้น​ทั้ง​ตาราง ไม่มี​แถว​ไหน​หาย​ไป และ​ศูนย์​แปล​ว่า​ไม่มี​แถว​ไหน​ได้​เครื่องหมาย​นั้น​ติด​มา สอง​ค่า​นี้​ปิด​ความ​เป็น​ไป​ได้ที่​ว่า คำ​สั่ง​ทำงาน​สำเร็จ​แล้ว​รายงาน​ผิด

แต่​ข้อ​ที่​ต้อง​อ่าน​ให้​ครบ​คือ​ข้อ​กลับ​กัน ศูนย์​แถว​ไม่​ได้​แปล​ว่า​คำ​สั่ง​ไม่​ได้​ถูก​ส่ง​ไป คำ​สั่ง​เดิน​ทาง​ไป​ถึง​ฐาน​ข้อมูล ถูก​วางแผน และ​ถูก​สั่ง สิ่ง​ที่​เป็น​ศูนย์​คือ​จำนวน​แถว​ที่​มัน​ได้​รับ​อนุญาต​ให้​แตะ

ทรง​เดียวกัน​ให้การ​ปฏิเสธ​สอง​แบบ และ​แบบ​ที่​ระบบ​เฝ้า​ง่าย​กว่า คือ​แบบ​ที่​ดัง​กว่า

คำ​สั่งคีย์ค่าที่​วัด​ได้สิ่ง​ที่​ผู้​เรียก​เห็น
insertselOnlyInsert42501 -1ข้อ​ผิด​พลาด พร้อม​รหัส​ที่​จับ​ได้
updateselOnlyUpdate00000 0สำเร็จ กระทบ​ศูนย์​แถว
deleteselOnlyDelete00000 0สำเร็จ กระทบ​ศูนย์​แถว

สอง​แถว​ล่าง​คือ​รูป​เดียว​กับ​ที่​คำ​สั่ง​ซึ่ง​เงื่อนไข where ไม่​ตรง​กับ​แถว​ไหน​เลย กลับ​มา​ด้วย แอป​ที่​เขียน​ไว้​ว่า “สำเร็จ​ก็​ไป​ต่อ” จึง​ไป​ต่อ และ log ไม่มี​บรรทัด​ให้​ค้น

เอกสาร​ของ PostgreSQL 18 ระบุ​ว่า เมื่อ​เปิด row-level security แล้ว​ไม่มี policy ใบ​ไหน ใช้ได้​กับ​คำ​สั่ง​นั้น ค่า​ปริยาย​คือ​ปฏิเสธ ข้อ​นี้​เป็น​ข้อ​อ้าง​เรื่อง​กลไก จึง​อ้าง​เอกสาร ส่วน​ตัวเลข​สี่​ตัว​ข้าง​บน​เป็น​สิ่ง​ที่ probe วัด

การ​ปฏิเสธ​สอง​แบบ​นี้​ไม่​ได้​ต่าง​กัน​ที่​ความ​รุนแรง มัน​ต่าง​กัน​ที่​คำ​สั่ง​มี​แถว​เดิม​ให้​คัด​หรือ​ไม่ update กับ delete คัด​แถว​เดิม​ได้​ศูนย์​แถว แล้ว​จบ​ตาม​ปกติ ส่วน insert ไม่มี​แถว​เดิม​ให้​คัด แถว​ใหม่​ของ​มัน​จึง​ต้อง​ถูก​ตัดสิน และ​คำ​ตอบ​คือ​ปฏิเสธ

ครึ่ง​หลัง​ของ​บท​ย้าย​ไป​ตาราง t_combine ซึ่ง​เริ่ม​จาก​ทรง​ที่​ว่าง​ที่สุด​ที่​เป็น​ไป​ได้

ตาราง t_combine หว่าน​ด้วย​เมล็ด​ชุด​เดียว​กับ t_selonly คือ​หก​แถว สาม​แถว​ต่อ​ผู้​เช่า และ​เปิด row-level security ไว้​แล้ว ต่าง​กัน​ที่​ตอน​นี้​ยัง​ไม่มี​ใคร​สั่ง create policy เลย​สัก​ใบ

ทรง​ที่ ⑫ · ตาราง t_combine · session ของ kaen_rls_app
select 'KAEN|combineZeroPolicies|'||count(*) from t_combine where true;

combineZeroPolicies = 0

ตาราง​มี​หก​แถว​อยู่​ครบ role ใบ​นี้​มี​สิทธิ์ select และ​ไม่มี policy ใบ​ไหน​ปฏิเสธ​มัน เพราะ​ไม่มี policy สัก​ใบ ค่าที่​ได้​คือ​ศูนย์

ตาราง​ที่​ไม่มี policy จึง​ไม่ใช่​ตาราง​ที่​เปิด​กว้าง มัน​คือ​ตาราง​ที่​ปิด​สนิท สำหรับ​ทุก​คน​ที่​ไม่ใช่​เจ้าของ​ตาราง ไม่ใช่ superuser และ​ไม่​ถือ​ธง BYPASSRLS สาม​ข้อ​ยกเว้น​นั้น​คือ​สิ่ง​ที่​บท​ที่ 1 วัด​ไว้​แล้ว

ใบ​ที่​สอง​เพิ่ม​แถว​เข้า​มา ไม่​ได้​ตัด​แถว​ออก

ทรง​ที่ ⑫ · เจ้าของ​ตาราง​เพิ่ม policy สอง​ใบ ชนิด​ปริยาย
create policy p_a on t_combine for select using (tenant_id = 'clinic-a');
create policy p_b on t_combine for select using (tenant_id = 'clinic-b');
ทรง​ที่ ⑫ · session ของ kaen_rls_app · ไม่​ต้อง​ตั้ง app.tenant
select 'KAEN|combineTwoPermissive|'||count(*) from t_combine where true;

combineTwoPermissive = 6

นิพจน์​สอง​ใบ​นี้​ตัด​กัน​ไม่​ได้ แถว​หนึ่ง​เป็น​ของ clinic-a หรือ​ของ clinic-b อย่าง​ใด​อย่าง​หนึ่ง ถ้า​เงื่อนไข​สอง​ใบ​ถูก​รวม​ด้วย AND ค่าที่​ได้​ต้อง​เป็น​ศูนย์ ค่าที่​วัด​ได้​คือ​หก ซึ่ง​เท่ากับ​ทั้ง​ตาราง

policy ชนิด​ปริยาย​เรียก​ว่า permissive policypermissive policypolicy ชนิด​ปริยาย ซึ่ง​หลาย​ใบ​รวม​กัน​ด้วย OR · บน​ตาราง t_combine สอง​ใบ​แบบ​นี้​ให้ 6 แถว ส่วน​นิพจน์​คู่​เดียวกัน​ที่​ใบ​ที่​สอง​ถูก​ประกาศ​เป็น restrictive ให้ 0 แถว · ชนิด​ของ policy จึง​พลิก​ผล​ได้​ทั้ง​ที่​นิพจน์​ไม่​เปลี่ยน และ​หลาย​ใบ​รวม​กัน​ด้วย OR ใบ​ที่​เพิ่ม​เข้า​มา​จึง​ขยาย​สิ่ง​ที่​มอง​เห็น​ได้​เสมอ ไม่มี​ทาง​ลด​มัน​ลง

คำ​ที่​เพิ่ม​เข้า​มา​คือ as restrictive และ​ไม่มี​ตัว​อักษร​ไหน​ใน​นิพจน์​ถูก​แตะ

ทรง​ที่ ⑫ · เจ้าของ​ตาราง​ประกาศ​ใบ​ที่​สอง​ใหม่ ด้วย​นิพจน์​เดิม
drop policy p_b on t_combine;
create policy p_b on t_combine as restrictive for select using (tenant_id = 'clinic-b');
ทรง​ที่ ⑫ · session ของ kaen_rls_app · คำ​สั่ง​เดิม​ทุก​ตัว​อักษร
select 'KAEN|combinePermissiveRestrictive|'||count(*) from t_combine where true;

combinePermissiveRestrictive = 0

หัวข้อ​ก่อน​หน้า​ให้ 6 จาก​นิพจน์​คู่​นี้ หัวข้อ​นี้​ให้ 0 จาก​นิพจน์​คู่​เดียวกัน สิ่ง​ที่​เปลี่ยน​คือ​ชนิด​ของ​ใบ​ที่​สอง restrictive policyrestrictive policypolicy ที่​รวม​กับ​ใบ​อื่น​ด้วย AND · บน​ตาราง t_combine permissive ของ​ผู้​เช่า​บวก restrictive ที่​เขียน​ว่า not archived ให้ 2 แถว · ส่วน restrictive ใบ​เดียว​โดดๆ บน​ตาราง​ใบ​เดียวกัน ให้ 0 แถว เพราะ​ไม่มี permissive ใบ​ไหน​อนุญาต​อะไร​ไว้​ก่อน รวม​กับ​ใบ​อื่น​ด้วย AND และ​เงื่อนไข​สอง​ข้อ​นี้​เป็น​จริง​พร้อม​กัน​ไม่​ได้

ชนิด​ของ policy ไม่​ได้​อยู่​ใน​นิพจน์ มัน​อยู่​ใน​คำ​สั่ง​ที่​สร้าง​ใบ​นั้น คน​ที่​อ่าน​นิพจน์​อย่าง​เดียว​เพื่อ​ตัดสิน​ว่า​ตาราง​เปิด​กว้าง​แค่​ไหน อ่าน​ข้อมูล​ไม่​ครบ

restrictive ที่​ทำงาน​เป็น​ตัว​ตัด และ restrictive ที่​โดด​เดี่ยว

หัวข้อ​ที่​มีชื่อ​ว่า “restrictive ที่​ทำงาน​เป็น​ตัว​ตัด และ restrictive ที่​โดด​เดี่ยว”

ใบ​เดิม​สอง​ใบ ถอด​ออก​ไป​หนึ่ง​ใบ แล้ว​สิ่ง​ที่​เหลือ​ไม่​ได้​อนุญาต​อะไร​เลย

ทรง​ที่ ⑫ · เจ้าของ​ตาราง​เปลี่ยน​ไป​ใช้ policy คู่​ใหม่
drop policy p_a on t_combine;
drop policy p_b on t_combine;
create policy p_t on t_combine for select using (tenant_id = current_setting('app.tenant', true));
create policy p_r on t_combine as restrictive for select using (not archived);
ทรง​ที่ ⑫ · session ของ kaen_rls_app · ตั้ง app.tenant เป็น clinic-a
set app.tenant = 'clinic-a';
select 'KAEN|combineTenantAndNotArchived|'||count(*) from t_combine where true;

combineTenantAndNotArchived = 2

ใบ p_t เป็น permissive และ​อนุญาต​เฉพาะ​แถว​ของ​ผู้​เช่า​ที่ app.tenant ชี้​ไว้ ใบ p_r เป็น restrictive และ​ตัด​แถว​ที่ archived เป็น​จริง​ออก ค่าที่​วัด​ได้​คือ 2 นี่​คือ​ทรง​ที่ restrictive ทำ​ใน​สิ่ง​ที่​คน​คาด​หวัง​จาก​มัน

ทรง​ที่ ⑫ · เจ้าของ​ตาราง​ถอด​ใบ permissive ออก เหลือ​ใบ restrictive ใบ​เดียว
drop policy p_t on t_combine;
ทรง​ที่ ⑫ · session ของ kaen_rls_app · ตั้ง app.tenant เป็น clinic-a เหมือน​เดิม
set app.tenant = 'clinic-a';
select 'KAEN|combineRestrictiveAlone|'||count(*) from t_combine where true;

combineRestrictiveAlone = 0

ใบ​ที่​เหลือ​อยู่​คือ p_r ซึ่ง​เขียน​ว่า not archived และ​เมล็ด​ตั้ง​ต้น​มี​แถว​ที่​ผ่าน​เงื่อนไข​นั้น​อยู่ ถึง​อย่าง​นั้น​ค่าที่​วัด​ได้​ก็​เป็น​ศูนย์ probe อ่าน​จาก pg_policy ยืนยัน​สอง​ค่า​ไว้​ใน​รอบ​เดียวกัน คือ combinePolicyCount = 1 และ combinePermissiveLeft = 0

restrictive ใบ​เดียว​ไม่​เคย​อนุญาต​อะไร มัน​ตัด​จาก​สิ่ง​ที่ permissive อนุญาต​ไว้​ก่อน เมื่อ​ไม่มี permissive สัก​ใบ สิ่ง​ที่​ถูก​อนุญาต​ไว้​ก่อน​คือ​เซต​ว่าง และ​การ​ตัด​เซต​ว่าง​ด้วย​เงื่อนไข​ใด​ก็ตาม ยัง​ได้​เซต​ว่าง

ครึ่ง​แรก​วัด​เสียง​ของ​การ​ปฏิเสธ ครึ่ง​หลัง​วัด​ว่า​ใบ​ไหน​อนุญาต​และ​ใบ​ไหน​ตัด

flowchart TB
  S1["ทรงที่ ⑥ · t_selonly · policy ใบเดียว ระบุ FOR SELECT"] --> S2{"คำสั่งมีแถวเดิมให้คัดหรือไม่"}
  S2 -->|"update และ delete · มีแถวเดิม"| S3["00000 กระทบ 0 แถว · selOnlyUpdate และ selOnlyDelete"]
  S2 -->|"insert · ไม่มีแถวเดิม"| S4["42501 · selOnlyInsert"]
  S3 --> S5["เจ้าของนับหลังจบ · 6 แถว และ 0 แถวที่ถูกแก้ · selOnlyRowsAfter และ selOnlyBangs"]
  S4 --> S5
  C1["ทรงที่ ⑫ · t_combine · หกแถวเท่ากัน"] --> C2["ไม่มี policy สักใบ · 0 แถว · combineZeroPolicies"]
  C1 --> C3["permissive สองใบ รวมด้วย OR · 6 แถว · combineTwoPermissive"]
  C1 --> C4["นิพจน์คู่เดิม ใบที่สองเป็น restrictive รวมด้วย AND · 0 แถว · combinePermissiveRestrictive"]
  C1 --> C5["permissive ผู้เช่า บวก restrictive not archived · 2 แถว · combineTenantAndNotArchived"]
  C1 --> C6["restrictive ใบเดียว ไม่มี permissive เหลือ · 0 แถว · combineRestrictiveAlone"]

คำ​บรรยาย​ภาพ: สอง​ทรง​ที่​บท​นี้​วัด ทรง​ที่ ⑥ แยก​การ​ปฏิเสธ​ออก​เป็น​สอง​เสียง​ตาม​ว่า​คำ​สั่ง มี​แถว​เดิม​ให้​คัด​หรือ​ไม่ ส่วน​ทรง​ที่ ⑫ ใช้​ตาราง​หก​แถว​ใบ​เดียว​กับ policy ห้า​ชุด แล้ว​ได้​จำนวน​แถว​ห้า​ค่า ซึ่ง​มี​เพียง​สาม​ค่าที่​ต่าง​กัน

ตัวเลข​ทั้ง​บท​มา​จาก SQL ที่​รัน​จริง ส่วน​กฎ​ว่า​ชนิด​ไหน​รวม​ด้วย​ตัว​ไหน มา​จาก​เอกสาร

spike.py รัน​ชุด​ข้อ​อ้าง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง แล้ว​เทียบ​ผล​ที​ละ​คีย์ รอบ​ที่​รัน​เมื่อ 2026-09-06 ได้ 52 ข้อ​อ้าง​ตรง​ทั้งหมด 98 ค่าที่​วัด​ได้ และ 0 ค่าที่​ขยับ​ระหว่าง​รอบ

สิ่ง​ที่​บท​นี้ ไม่​ได้ พิสูจน์​คือ​กลไก​ข้าง​ใน บท​นี้​ไม่​ได้​แสดง​ว่า​ตัว​ตัดสิน ประกอบ​นิพจน์​ของ​สอง​ชนิด​เข้า​ด้วย​กัน​ด้วย​ลำดับ​ใด ข้อ​อ้าง​แบบ​นั้น​อ้าง​เอกสาร​ของ PostgreSQL 18 และ​เอกสาร​เป็น​คนละ​หลัก​ฐาน​กับ probe

ตัวเลข​ทุก​ตัว​ผูก​กับ​รุ่น​ที่​ระบุ​ไว้

ผล​ทั้งหมด​ใน​บท​นี้​วัด​บน PostgreSQL 18.6 บน aarch64-unknown-linux-musl ใน container ของ​คอร์ส​นี้​เอง major อื่น​เป็น​คนละ​การ​วัด และ​บท​นี้​ไม่​ได้​อ้าง​ถึง​มัน

หน่วย​ของ​หลัก​ฐาน​ใน​บท​นี้​มี​สี่​หน่วย คือ​จำนวน​แถว​ที่​ผู้​อ่าน​นับ​ได้ จำนวน​แถว​ที่​คำ​สั่ง​กระทำ จำนวน policy ที่​ผูก​กับ​ตาราง และ​รหัส SQLSTATE ทั้ง​คอร์ส​ไม่มี​หน่วย​เวลา​สัก​ตัว

รู้​แล้ว​ว่า policy ปฏิเสธ​ด้วย​เสียง​ไหน​บ้าง ยัง​ไม่รู้​ว่า​มี​ทาง​ไหน​ที่​มัน​ไม่​ได้​ถูก​เรียก​เลย

ทุก​ตัวเลข​ใน​บท​นี้​มา​จาก​คำ​สั่ง​ที่​ยิง​ตรง​เข้า​ตาราง t_selonly และ t_combine เมื่อ​คำ​สั่ง​ชี้​ตรง​ไป​ที่​ตาราง policy ที่​ผูก​กับ​ตาราง​นั้น​จะ​ได้​ตัดสิน​เสมอ ไม่​ว่า​คำ​ตอบ​ของ​มัน​จะ​เป็น​อนุญาต หรือ​เป็นการ​ปฏิเสธ​เสียง​ใด​เสียง​หนึ่ง​ใน​สอง​เสียง

แต่​แอป​จริง​ไม่​ได้​ชี้​ตรง​เข้า​ตาราง​เสมอ​ไป มัน​เรียก​ผ่าน view และ​เรียก​ผ่าน​ฟังก์ชัน คำถาม​คือ​ทาง​อ้อม​เหล่า​นั้น​เดิน​ผ่าน policy ใบ​เดิม หรือ​เดิน​อ้อม​มัน​ไป

บท​ที่ 5 วัด​สี่​ทาง​เข้า​บน​ตาราง​ใบ​เดียวกัน แล้ว​เทียบ​จำนวน​แถว​ที่​แต่ละ​ทาง​คืน​กลับ​มา ทาง​ที่​ให้​ค่า​ต่าง​จาก​การ​อ่าน​ตรงๆ คือ​ทาง​ที่ policy มอง​ไม่​เห็น


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • PostgreSQL 18 — Row Security Policies (เข้า​ถึง 2026-09-06) — ที่มา​ของ​กฎ​ว่า​ค่า​ปริยาย​คือ​ปฏิเสธ​เมื่อ​ไม่มี policy ใบ​ไหน​ใช้ได้​กับ​คำ​สั่ง​นั้น และ​กฎ​ว่า permissive รวม​กัน​ด้วย OR ส่วน restrictive รวม​ด้วย AND

  • PostgreSQL 18 — CREATE POLICY (เข้า​ถึง 2026-09-06) — ความ​หมาย​ของ AS PERMISSIVE AS RESTRICTIVE และ FOR SELECT

  • PostgreSQL 18 — PostgreSQL Error Codes (เข้า​ถึง 2026-09-06) — ที่มา​ของ​รหัส 42501 และ 00000 ใน​ตาราง​ของ​บท​นี้

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-rls/spike.py ซึ่ง​สร้าง​ฐาน​ข้อมูล​กับ role ขึ้น​ใหม่​ทุกร​อบ แล้ว​รื้อ​ทิ้ง​เมื่อ​จบ · ต้อง​สั่ง scripts/clinic-rls/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55438

เช็กความเข้าใจ — บทที่ 4

ข้อ 1 / 3

ตาราง `t_selonly` มี policy ใบเดียวที่ระบุ `for select` app role ที่ตั้ง `app.tenant` เป็น `clinic-a` สั่ง `update` แถวของผู้เช่านั้น ได้อะไรกลับมา