ตารางใบเดิม ทางเข้าห้าทาง และสองทางในนั้นให้หกแถว
ผู้อ่านในบทนี้ไม่ใช่เจ้าของตาราง มันคือ role ชื่อ kaen_rls_app ซึ่งตั้ง app.tenant
เป็น clinic-a แล้วนับแถวของตาราง t_view ตรงๆ ค่าที่วัดได้คือ noForceBase = 3
คำสั่งถัดไปใน session เดียวกัน นับผ่าน view ที่เจ้าของตารางสร้างไว้
ซึ่งชี้ไปที่ตารางใบเดิมทุกแถว ค่าที่วัดได้คือ noForceViewDefault = 6
สองคำสั่งนั้นอยู่ติดกัน ตารางใบเดียว policy ใบเดียว และค่าตั้งของ session ตัวเดียว บทนี้นับทางเข้าทั้งหมดที่ probe วัด แล้วถามว่าอะไรปิดทางที่ให้หกได้บ้าง
คอร์สนี้อยู่ในซีรีส์คลินิกเดียวกับ คอร์สคัดกรองผู้เรียก และ คอร์ส CORS กับ CSRF แต่บทนี้ไม่มีแอปมาเกี่ยวข้อง มีแต่ตาราง view ฟังก์ชัน 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
ตารางใบเดิม view สองใบ และฟังก์ชันสองใบ
หัวข้อที่มีชื่อว่า “ตารางใบเดิม view สองใบ และฟังก์ชันสองใบ”ทุกทางเข้าในบทนี้ชี้ไปที่ตารางใบเดียวกัน และเจ้าของตารางเป็นคนสร้างทุกใบ
create table t_view ( id bigserial primary key, tenant_id text not null, patient text not null, archived boolean not null default false);insert into t_view (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_view to kaen_rls_app, kaen_rls_bypass;grant usage, select on sequence t_view_id_seq to kaen_rls_app, kaen_rls_bypass;alter table t_view enable row level security;create policy p on t_view for all using (tenant_id = current_setting('app.tenant', true));create view v_def as select * from t_view;create view v_inv with (security_invoker = true) as select * from t_view;grant select on v_def, v_inv to kaen_rls_app;create function public.f_def() returns bigint language sql security definer as $$ select count(*) from t_view $$;create function public.f_inv() returns bigint language sql security invoker as $$ select count(*) from t_view $$;เมล็ดเป็นรูปเดียวกับบทที่ 1 คือหกแถว สามแถวต่อผู้เช่า และ policy ใบเดียวที่เทียบ
tenant_id กับค่าตั้งของ session ตรงไปตรงมา
ก้อนนี้สั่ง enable row level security อย่างเดียว ยังไม่มีบรรทัดไหนสั่ง force
บทที่ 1 วัดไว้แล้วว่าเจ้าของตารางไม่ได้อยู่ใต้ policy ของตัวเองด้วยคำสั่ง enable เพียงคำสั่งเดียว
ห้าคำสั่งล่างคือของที่บทนี้เพิ่มเข้ามา view สองใบต่างกันที่คำว่า
security_invokersecurity_invoker (view option)ค่าตั้งของ view ที่ให้ใช้สิทธิ์ของผู้เรียกแทนของเจ้าของ view · ใต้ทรงที่ยังไม่ FORCE view ธรรมดาของเจ้าของบนตาราง t_view ให้ 6 แถว (noForceViewDefault) ส่วน view ที่ตั้งค่านี้เป็น true ให้ 3 แถว (noForceViewInvoker) · ค่าตั้งนี้มีใน PostgreSQL ตั้งแต่ major 15 ใบเดียว และฟังก์ชันสองใบต่างกันที่คำว่า SECURITY DEFINERSECURITY DEFINERฟังก์ชันที่ทำงานด้วยสิทธิ์ของผู้สร้างมัน ไม่ใช่ของผู้เรียก · ใต้ ENABLE ที่ยังไม่ FORCE ผู้เรียกอ่านตาราง t_view ตรงๆ ได้ 3 แถว (noForceBase) แต่เรียกผ่านฟังก์ชันแบบนี้ของเจ้าของตารางได้ 6 แถว (noForceSecDefiner) · สั่ง FORCE แล้วทางนี้ได้ 3 แถว (forceSecDefiner) เท่ากับทางตรง
กับ security invoker ใบเดียวเช่นกัน ตัว SQL ข้างในเหมือนกันทุกตัวอักษร
ยังไม่สั่ง FORCE · ห้าทางให้สองค่า
หัวข้อที่มีชื่อว่า “ยังไม่สั่ง FORCE · ห้าทางให้สองค่า”ห้าคำสั่งใน session เดียว ชี้ไปที่ตารางใบเดียวกัน แล้วได้ 3 สามครั้ง และ 6 สองครั้ง
set app.tenant = 'clinic-a';select 'KAEN|noForceBase|'||count(*) from t_view where true;select 'KAEN|noForceViewDefault|'||count(*) from v_def where true;select 'KAEN|noForceViewInvoker|'||count(*) from v_inv where true;select 'KAEN|noForceSecDefiner|'||public.f_def();select 'KAEN|noForceSecInvoker|'||public.f_inv();| ทางเข้า | คีย์ | แถวที่นับได้ |
|---|---|---|
อ่านตาราง t_view ตรงๆ | noForceBase | 3 |
ผ่าน view v_def ที่ไม่ได้ตั้งค่าอะไรเพิ่ม | noForceViewDefault | 6 |
ผ่าน view v_inv ที่ตั้ง security_invoker = true | noForceViewInvoker | 3 |
เรียก public.f_def() ซึ่งเป็น security definer | noForceSecDefiner | 6 |
เรียก public.f_inv() ซึ่งเป็น security invoker | noForceSecInvoker | 3 |
สองแถวที่ให้ 6 คือแถวที่นับได้ทั้งตาราง ผู้อ่านคนเดิมใน session เดียวกันเห็นแถวของ
clinic-b ครบทุกแถว ทั้งที่ค่าตั้งของเขายังเป็น clinic-a อยู่
เอกสารของ PostgreSQL 18 อธิบายว่า view ที่ไม่ได้ตั้งค่าเพิ่ม อ่านตารางที่อยู่ข้างใต้
ด้วยสิทธิ์ของผู้เป็นเจ้าของ view และฟังก์ชัน security definer ทำงานด้วยสิทธิ์ของ
ผู้เป็นเจ้าของฟังก์ชัน ทั้งสองใบในทรงนี้เป็นของเจ้าของตาราง
ข้อนั้นเป็นข้ออ้างเรื่องกลไก จึงอ้างเอกสาร ส่วนตัวเลข 3 กับ 6 เป็นสิ่งที่ probe วัด บทนี้ไม่ได้พิสูจน์ว่าตัวตัดสินอ่านสิทธิ์ของใครก่อนใคร
สองใบที่ให้ 3 คือใบที่ประกาศว่าใช้สิทธิ์ของผู้เรียก noForceViewInvoker และ
noForceSecInvoker เท่ากับ noForceBase พอดี ทั้งที่คำสั่งเดินผ่าน view
และผ่านฟังก์ชันคนละทาง
ธงของ view ที่อ่านออกมาจาก catalog
หัวข้อที่มีชื่อว่า “ธงของ view ที่อ่านออกมาจาก catalog”ค่าตั้งที่แยก
v_invออกจากv_defถูกอ่านออกมาจาก catalog จริง ไม่ใช่เดาจากคำสั่งที่สั่งไป
select 'KAEN|viewInvokerFlag|'|| coalesce(reloptions::text like '%security_invoker=true%', false)from pg_class where oid = 'v_inv'::regclass;viewInvokerFlag = true
คีย์นี้มีอยู่เพราะ view สองใบต่างกันด้วยคำในบรรทัด create view เพียงคำเดียว
และคำในบรรทัดที่สั่งไป ไม่ใช่หลักฐานว่าฐานข้อมูลเก็บค่านั้นไว้จริง
ธงใบนี้ถูกวัดกับ v_inv ใบเดียว probe ไม่ได้อ่านค่าตั้งของ v_def ออกมา
และไม่ได้อ่านของฟังก์ชันสองใบด้วย ทางที่แยกฟังก์ชันสองใบออกจากกันในบทนี้
คือจำนวนแถวที่มันคืนกลับมา
security_invoker ของ view มาใน PostgreSQL 15 ตาม release notes ของรุ่นนั้น
ทรงที่วัด noForceViewInvoker จึงวัดไม่ได้เลยบน major ที่เก่ากว่านั้น
และ container ของคอร์สนี้ตรึงไว้ที่ 18.6
สั่ง FORCE แล้วห้าทางให้ค่าเดียวกัน
หัวข้อที่มีชื่อว่า “สั่ง FORCE แล้วห้าทางให้ค่าเดียวกัน”คำสั่งเดียวที่เจ้าของตารางสั่ง แล้วสองทางที่ให้หก ลงมาเท่ากับอีกสามทาง
alter table t_view force row level security;set app.tenant = 'clinic-a';select 'KAEN|forceBase|'||count(*) from t_view where true;select 'KAEN|forceViewDefault|'||count(*) from v_def where true;select 'KAEN|forceViewInvoker|'||count(*) from v_inv where true;select 'KAEN|forceSecDefiner|'||public.f_def();select 'KAEN|forceSecInvoker|'||public.f_inv();| ทางเข้า | คีย์ | แถวที่นับได้ |
|---|---|---|
อ่านตาราง t_view ตรงๆ | forceBase | 3 |
ผ่าน view v_def | forceViewDefault | 3 |
ผ่าน view v_inv | forceViewInvoker | 3 |
เรียก public.f_def() | forceSecDefiner | 3 |
เรียก public.f_inv() | forceSecInvoker | 3 |
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 ด้วย สองทางที่เดินด้วยสิทธิ์ของเจ้าของ จึงเดินเข้ามาอยู่ใต้ policy ตามไปด้วย
ค่าที่ขยับคือสองตัวที่เคยเป็นหก forceViewDefault และ forceSecDefiner
เป็น 3 ส่วนอีกสามตัวเท่าเดิมทุกตัว การสั่ง force จึงไม่ได้เปลี่ยนทางที่ปิดอยู่แล้ว
ผู้เรียกที่ไม่ตั้งค่านั้นเลย
หัวข้อที่มีชื่อว่า “ผู้เรียกที่ไม่ตั้งค่านั้นเลย”session ถัดไปไม่มีใครสั่ง set app.tenant แล้ววัดสองทางที่เพิ่งถูกปิดซ้ำ
select 'KAEN|forceNoGucViewDefault|'||count(*) from v_def where true;select 'KAEN|forceNoGucSecDefiner|'||public.f_def();forceNoGucViewDefault = 0 และ forceNoGucSecDefiner = 0
policy ใบเดิมอ่านค่าตั้งที่ยังไม่มีใครตั้ง แล้วไม่มีแถวไหนผ่าน ทั้งทางของ view และทางของฟังก์ชัน สองทางนี้จึงพาค่าตั้งของ session ที่เรียกมันติดไปด้วย
สองคีย์นี้วัดแค่สองทาง probe ไม่ได้วัดทางตรงและไม่ได้วัดสองทางที่ประกาศว่าใช้
สิทธิ์ของผู้เรียกในทรงนี้ ตัวเลข 0 จึงเป็นของ v_def กับ public.f_def() เท่านั้น
สลับ policy ไปอ่าน current_user แล้วห้าทางแตกอีกครั้ง
หัวข้อที่มีชื่อว่า “สลับ policy ไปอ่าน current_user แล้วห้าทางแตกอีกครั้ง”
forceยังเปิดอยู่ ตารางใบเดิม view ใบเดิม ฟังก์ชันใบเดิม เปลี่ยนที่นิพจน์ของ policy อ่าน
drop policy p on t_view;create policy p on t_view for all using (tenant_id = 'clinic-a' and current_user = 'kaen_rls_app');select 'KAEN|currentUserBase|'||count(*) from t_view where true;select 'KAEN|currentUserViewDefault|'||count(*) from v_def where true;select 'KAEN|currentUserViewInvoker|'||count(*) from v_inv where true;select 'KAEN|currentUserSecDefiner|'||public.f_def();select 'KAEN|currentUserSecInvoker|'||public.f_inv();| ทางเข้า | คีย์ | แถวที่นับได้ |
|---|---|---|
อ่านตาราง t_view ตรงๆ | currentUserBase | 3 |
ผ่าน view v_def | currentUserViewDefault | 3 |
ผ่าน view v_inv | currentUserViewInvoker | 3 |
เรียก public.f_def() | currentUserSecDefiner | 0 |
เรียก public.f_inv() | currentUserSecInvoker | 3 |
force ยังเปิดอยู่ทั้งทรง ไม่มีใครสั่ง no force และไม่มีใครแตะ view หรือฟังก์ชันสักใบ
สิ่งที่เปลี่ยนคือนิพจน์ของ policy อ่านของคนละอย่าง และ session ของห้าคำสั่งนี้
ไม่ได้สั่ง set app.tenant เลย เพราะนิพจน์ใบใหม่ไม่ได้อ่านค่าตั้งนั้น
ทางของ SECURITY DEFINER ให้ 0 ทางเดียว ในทรงก่อนหน้า ทางเดียวกันนี้ให้
forceSecDefiner = 3 นิพจน์ใหม่เทียบ current_user กับ kaen_rls_app
และเอกสารระบุว่าฟังก์ชันแบบนี้ทำงานด้วยสิทธิ์ของผู้เป็นเจ้าของมัน
ค่าที่วัดได้สอดคล้องกับข้อที่เอกสารกำหนด บทนี้ไม่ได้พิสูจน์ว่า current_user
ถูกประเมินเป็นชื่อใดข้างในแต่ละทาง มันวัดจำนวนแถวที่ผู้เรียกได้กลับไปเท่านั้น
ห้าทาง สามสถานะ และทางที่ขยับ
หัวข้อที่มีชื่อว่า “ห้าทาง สามสถานะ และทางที่ขยับ”เอาสามตารางข้างบนมาวางซ้อนกัน แล้วจะเห็นว่าคีย์ที่ขยับมีอยู่สองคีย์ และมันขยับคนละครั้ง
flowchart LR S["kaen_rls_app · ตั้ง app.tenant เป็น clinic-a"] --> D1["อ่าน t_view ตรงๆ"] S --> D2["ผ่าน v_def · view ที่ไม่ได้ตั้งค่าเพิ่ม"] S --> D3["ผ่าน v_inv · security_invoker = true"] S --> D4["เรียก public.f_def · security definer"] S --> D5["เรียก public.f_inv · security invoker"] D1 --> R1["noForceBase 3 · forceBase 3 · currentUserBase 3"] D2 --> R2["noForceViewDefault 6 · forceViewDefault 3 · currentUserViewDefault 3"] D3 --> R3["noForceViewInvoker 3 · forceViewInvoker 3 · currentUserViewInvoker 3"] D4 --> R4["noForceSecDefiner 6 · forceSecDefiner 3 · currentUserSecDefiner 0"] D5 --> R5["noForceSecInvoker 3 · forceSecInvoker 3 · currentUserSecInvoker 3"]
คำบรรยายภาพ: ห้าทางเข้าที่ probe วัด และค่าของแต่ละทางในสามสถานะเรียงจากซ้ายไปขวา
คือยังไม่สั่ง force สั่ง force แล้ว และเปลี่ยน policy ไปอ่าน current_user
สามแถวที่ไม่ขยับเลยสักสถานะคือ R1 ทางตรง R3 v_inv และ R5 f_inv
ส่วนสองแถวที่ขยับคือ R2 v_def และ R4 f_def และมันขยับคนละครั้งกัน
v_def ขยับตอนสั่ง force แล้วนิ่งหลังจากนั้น ส่วน public.f_def() ขยับตอนสั่ง
force เหมือนกัน แล้วขยับอีกครั้งตอน policy เปลี่ยนไปอ่านของคนละอย่าง
ไม่มีทางไหนเป็นยาแก้ของอีกทาง
หัวข้อที่มีชื่อว่า “ไม่มีทางไหนเป็นยาแก้ของอีกทาง”ตัวที่ปิดทางได้ ขึ้นกับว่า policy อ่านอะไร ไม่ได้ขึ้นกับชื่อของกลไกที่ปิด
security_invoker ปิดทางของ view ได้ตั้งแต่ก่อนสั่ง force คือ
noForceViewInvoker = 3 ขณะที่ noForceViewDefault = 6 แต่มันปิดได้ทางเดียว
ทางของฟังก์ชันในสถานะเดียวกันยังเป็น noForceSecDefiner = 6
force ปิดได้ทั้งสองทางพร้อมกัน ในสถานะที่ policy อ่านค่าตั้งของ session
ห้าคีย์ในสถานะนั้นเป็น 3 ทั้งหมด เพราะค่าที่ policy อ่าน เดินไปกับ session ของผู้เรียก
ไม่ได้เดินไปกับสิทธิ์ที่คำสั่งใช้
แต่ force ที่ยังเปิดอยู่ตัวเดิม ไม่ได้ทำให้ห้าทางเท่ากันในสถานะที่สาม
currentUserSecDefiner = 0 ขณะที่อีกสี่คีย์เป็น 3 สิ่งที่ policy อ่านในสถานะนั้น
เป็นสิ่งที่ทางของฟังก์ชันเปลี่ยนไปด้วย
ทั้งสามอย่างจึงไม่ได้เรียงกันเป็นบันได คำถามที่ตอบได้คือ นิพจน์ของ policy อ่านของที่เดินไปกับ session หรืออ่านของที่เดินไปกับสิทธิ์ และคำตอบของข้อนั้น ตัดสินว่าทางไหนยังไม่ถูกปิด
ทั้งสามสถานะในบทนี้มี policy ผูกอยู่กับ t_view ตลอด และตัวเลขในตารางเดินจาก
6 ลงไป 3 และลงไป 0 โดยจำนวนใบไม่ได้เปลี่ยน
ค่าที่ต่างกันในสามสถานะมาจากสองอย่างที่อยู่คนละที่กับตัวนับ คือธงบนตาราง ซึ่งบทที่ 1 วัดออกมาจาก catalog และสิ่งที่นิพจน์ของ policy หยิบมาเทียบ
- บทที่ 1 · policy หนึ่งใบที่ไม่เคยถูกเรียกใช้
วัดธงสองใบออกมาจาก catalog และวัดว่า
forceทำอะไรกับตารางที่เจ้าของอ่านเอง - บทที่ 6 · การโจมตีที่ไม่ขยับจำนวนแถว รับช่วงต่อจากหัวข้อสุดท้ายของบทนี้ ด้วยทรงที่จำนวนแถวค้างที่เดิมทั้งก่อนและหลังการโจมตี
ที่วัดได้ กับที่เอกสารกำหนด
หัวข้อที่มีชื่อว่า “ที่วัดได้ กับที่เอกสารกำหนด”สิบแปดคีย์ในบทนี้มาจาก SQL ที่รันจริง ส่วนคำอธิบายว่าทางไหนเดินด้วยสิทธิ์ของใคร มาจากเอกสาร
spike.py รันชุดข้ออ้างทั้งชุดสามรอบต่อการเรียกหนึ่งครั้ง แล้วเทียบผลทีละคีย์
รอบที่รันเมื่อ 2026-09-06 ได้ 52 ข้ออ้างตรงทั้งหมด 98 ค่าที่วัดได้
และ 0 ค่าที่ขยับระหว่างรอบ
สิ่งที่บทนี้ ไม่ได้ พิสูจน์คือกลไกข้างใน บทนี้ไม่ได้แสดงว่าตัวตัดสินตั้งสิทธิ์ของ คำสั่งไว้เป็นชื่อใดตอนเดินผ่าน view และตอนเดินผ่านฟังก์ชัน ข้ออ้างแบบนั้น อ้างเอกสารของ PostgreSQL 18 และเอกสารเป็นคนละหลักฐานกับ probe
ผลทั้งหมดในบทนี้วัดบน PostgreSQL 18.6 บน aarch64-unknown-linux-musl
ใน container ของคอร์สนี้เอง major อื่นเป็นคนละการวัด และบทนี้ไม่ได้อ้างถึงมัน
หน่วยของหลักฐานในบทนี้มีสองหน่วย คือจำนวนแถวที่ผู้เรียกนับได้
และธงบูลีนที่อ่านออกมาจาก pg_class ทั้งคอร์สไม่มีหน่วยเวลาสักตัว
คำถามที่บทนี้ทิ้งไว้
หัวข้อที่มีชื่อว่า “คำถามที่บทนี้ทิ้งไว้”รู้แล้วว่าทางเข้ามีหลายทาง ยังไม่รู้ว่าหน่วยที่ใช้ดูมันอยู่ มองอะไรไม่เห็น
ทุกตัวเลขในบทนี้เป็นจำนวนแถวที่ผู้เรียกนับได้ และทุกครั้งที่ทางเข้าทางหนึ่งถูกปิด ตัวเลขของทางนั้นก็ขยับให้เห็น หน่วยนี้จึงทำงานได้ตลอดห้าทาง
แต่ทั้งสามสถานะข้างบนเปลี่ยนจำนวนแถวเสมอ ยังไม่มีทรงไหนในคอร์สที่ผู้เช่าซึ่งเป็น เจ้าของแถวเปลี่ยนไป โดยที่จำนวนแถวค้างอยู่ที่เดิม
บทที่ 6 วัดทรงนั้น policy เรียกฟังก์ชันที่ไม่ได้ตรึงค่าตั้งของการค้นชื่อ แล้วผู้โจมตีวางของตัวเองไว้ข้างหน้า ยอดรวมไม่ขยับสักแถว และหน่วยที่ใช้ทั้งบทนี้ มองไม่เห็นอะไรเลย
-
PostgreSQL 18 — Row Security Policies (เข้าถึง 2026-09-06) — ที่มาของกฎว่าเจ้าของตารางถูกยกเว้นจาก policy ตามค่าปริยาย และ
FORCEคือสิ่งที่ถอนการยกเว้นนั้น -
PostgreSQL 18 —
CREATE VIEW(เข้าถึง 2026-09-06) — ความหมายของค่าตั้งsecurity_invoker· หน้านี้เอ่ยชื่อค่าตั้งเจ็ดครั้ง แต่ไม่ระบุ major ที่มันเริ่มมีสักครั้ง -
PostgreSQL 15 — release notes §E.20.3.1.6 Privileges (เข้าถึง 2026-09-06) — major ที่
security_invokerเริ่มมี · “Allow table accesses done by a view to optionally be controlled by privileges of the view’s caller” -
PostgreSQL 18 —
CREATE FUNCTION(เข้าถึง 2026-09-06) — ความหมายของSECURITY DEFINERและSECURITY INVOKER -
PostgreSQL 18 — System Information Functions (เข้าถึง 2026-09-06) — ความหมายของ
current_userซึ่งนิพจน์ของ policy ในสถานะที่สามหยิบมาเทียบ -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/clinic-rls/spike.pyซึ่งสร้างฐานข้อมูลกับ role ขึ้นใหม่ทุกรอบ แล้วรื้อทิ้งเมื่อจบ · ต้องสั่งscripts/clinic-rls/up.shก่อน ซึ่งยก container ของคอร์สนี้เองขึ้นที่ port 55438
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3ตาราง `t_view` สั่ง `enable row level security` อย่างเดียว ยังไม่สั่ง `force` แล้ว app role ตั้ง `app.tenant` เป็น `clinic-a` นับผ่าน view ที่เจ้าของตารางสร้างไว้แบบไม่ตั้งค่าเพิ่ม ได้กี่แถว