03 มี.ค. 2568·อ่าน 2 นาที

แนวทาง RLS ของ PostgreSQL สำหรับแอปมัลติเทนแนนท์

เรียนรู้ PostgreSQL row-level security ด้วยแพทเทิร์นปฏิบัติสำหรับการแยกผู้เช่าและกฎบทบาท เพื่อบังคับใช้การเข้าถึงที่ระดับฐานข้อมูล ไม่ใช่แค่ในแอป

แนวทาง RLS ของ PostgreSQL สำหรับแอปมัลติเทนแนนท์

ทำไมการบังคับใช้ที่ฐานข้อมูลถึงสำคัญในแอปธุรกิจ

แอปธุรกิจมักมีกฎเช่น “ผู้ใช้เห็นได้เฉพาะข้อมูลของบริษัทตัวเอง” หรือ “มีเฉพาะผู้จัดการเท่านั้นที่อนุมัติการคืนเงินได้” ทีมงานหลายทีมบังคับกฎเหล่านี้ที่ UI หรือ API แล้วคิดว่าพอแล้ว แต่ปัญหาคือทุกเส้นทางเพิ่มเติมไปยังฐานข้อมูลเป็นโอกาสให้ข้อมูลรั่วได้: เครื่องมือแอดมินภายใน งานแบ็กกราวด์ คิวรีวิเคราะห์ endpoint ที่ลืมไว้ หรือบั๊กที่ข้ามการตรวจสอบ

การแยกผู้เช่า (tenant isolation) หมายความว่าลูกค้าหนึ่งราย (ผู้เช่า) จะไม่มีวันอ่านหรือแก้ไขข้อมูลของผู้เช่ารายอื่น แม้จะเผลอก็ตาม การเข้าถึงตามบทบาทหมายความว่าคนภายในผู้เช่าเดียวกันยังมีสิทธิ์ต่างกัน เช่น เจ้าหน้าที่ธรรมดา ผู้จัดการ ฝ่ายการเงิน กฎเหล่านี้อธิบายง่าย แต่ยากที่จะรักษาความสอดคล้องเมื่อกระจายอยู่หลายที่

PostgreSQL row-level security (RLS) เป็นฟีเจอร์ของฐานข้อมูลที่ให้ฐานข้อมูลตัดสินได้ว่าแต่ละคำขอเห็นหรือแก้ไขแถวใด แทนที่จะหวังว่าทุกคิวรีในแอปจะจำ WHERE ที่ถูกต้อง ฐานข้อมูลจะนำกฎมาใช้ให้อัตโนมัติ

RLS ไม่ใช่โล่วิเศษสำหรับทุกอย่าง มันไม่ออกแบบสคีมาให้แทนคุณ ไม่ทดแทนการพิสูจน์ตัวตน และไม่ป้องกันคนที่มีบทบาทฐานข้อมูลทรงพลังอยู่แล้ว (เช่น superuser) นอกจากนี้มันจะไม่ป้องกันความผิดพลาดด้านตรรกะเช่น “คนสามารถอัปเดตแถวที่ตนไม่สามารถเลือกได้” เว้นแต่ว่าคุณเขียนนโยบายทั้งอ่านและเขียน

สิ่งที่คุณได้คือตาข่ายความปลอดภัยที่แข็งแรง:

  • ชุดกฎเดียวสำหรับทุกเส้นทางโค้ดที่เข้าถึงฐานข้อมูล
  • ลดความผิดพลาดเมื่อฟีเจอร์ใหม่ถูกปล่อย
  • การตรวจสอบชัดเจนขึ้น เพราะกฎการเข้าถึงอยู่ใน SQL
  • การป้องกันที่ดีกว่าเมื่อบั๊กใน API หลุดผ่าน

มีค่าใช้จ่ายการตั้งค่าบ้าง คุณต้องมีวิธีสม่ำเสมอในการส่งข้อมูล "ใครเป็นผู้ใช้" และ "ผู้เช่าคือใคร" เข้าไปในฐานข้อมูล และต้องดูแลนโยบายเมื่อแอปเติบโต ผลตอบแทนค่อนข้างมาก โดยเฉพาะสำหรับ SaaS และเครื่องมือภายในที่เก็บข้อมูลลูกค้าที่สำคัญ

พื้นฐานของ Row-Level Security แบบไม่ใช้ศัพท์เทคนิค

Row-Level Security (RLS) จะกรองแถวที่คำขอสามารถเห็นหรือแก้ไขโดยอัตโนมัติ แทนที่จะพึ่งพาทุกหน้าจอ endpoint หรือรายงานให้ "จำ" กฎ ฐานข้อมูลจะใช้กฎให้คุณ

กับ PostgreSQL RLS คุณเขียนนโยบายที่จะถูกตรวจสอบสำหรับทุก SELECT, INSERT, UPDATE และ DELETE ถ้านโยบายบอกว่า "ผู้ใช้คนนี้เห็นได้เฉพาะแถวของผู้เช่า A" หน้าแอดมินที่ลืมไป คิวรีใหม่ หรือ hotfix ที่รีบร้อนก็ยังมีการป้องกันเหมือนกัน

RLS แตกต่างจาก GRANT/REVOKE GRANT ตัดสินว่าบทบาทใดเข้าถึงตารางได้หรือไม่ได้ (หรือคอลัมน์บางคอลัมน์) ส่วน RLS ตัดสินว่าแถวภายในตารางนั้น ๆ เข้าถึงได้เท่าไร ในการใช้งานจริง คุณมักใช้ทั้งคู่: GRANT เพื่อจำกัดว่าใครเข้าถึงตารางได้ และ RLS เพื่อจำกัดสิ่งที่พวกเขาเข้าถึง

มันยังทนทานในโลกที่ยุ่งเหยิงด้วย มุมมอง (views) โดยทั่วไปปฏิบัติตาม RLS เพราะการเข้าถึงตารางที่เป็นแหล่งข้อมูลยังคงเรียกนโยบาย การ join และ subquery ยังคงถูกกรอง ดังนั้นผู้ใช้จะไม่สามารถ "join" เพื่อเข้าถึงข้อมูลผู้อื่นได้ และนโยบายใช้ได้ไม่ว่าจะเป็นไคลเอนต์ใดที่รันคิวรี: โค้ดแอป, คอนโซล SQL, งานแบ็กกราวด์ หรือเครื่องมือรายงาน

RLS เหมาะเมื่อคุณต้องการการแยกผู้เช่าอย่างเข้มงวด มีหลายวิธีในการคิวรีข้อมูลเดียวกัน หรือมีหลายบทบาทที่ใช้ตารางร่วมกัน (พบได้บ่อยใน SaaS และเครื่องมือภายใน) มันอาจเกินความจำเป็นสำหรับแอปเล็ก ๆ ที่มีแบ็กเอนด์เชื่อถือได้เพียงตัวเดียว หรือข้อมูลที่ไม่สำคัญและไม่ออกนอกบริการเดียว หากคุณมีจุดเข้าใช้มากกว่าหนึ่งจุด (เครื่องมือแอดมิน, การส่งออก, BI, สคริปต์) RLS มักคุ้มค่าที่จะลงแรงตั้งแต่แรก

เริ่มจากการทำแผนแผนผังผู้เช่า บทบาท และความเป็นเจ้าของข้อมูล

ก่อนเขียนนโยบายสักอัน ให้ชัดเจนว่าใครเป็นเจ้าของอะไร PostgreSQL RLS ทำงานได้ดีที่สุดเมื่อโมเดลข้อมูลของคุณสะท้อนผู้เช่า บทบาท และความเป็นเจ้าของอยู่แล้ว

เริ่มจากผู้เช่า ในแอป SaaS ส่วนใหญ่ กฎง่าย ๆ คือ: ทุกตารางที่มีข้อมูลลูกค้าร่วมต้องมี tenant_id นั่นรวมถึงตารางที่ดู "ชัดเจน" เช่น ใบแจ้งหนี้ แต่ก็รวมถึงสิ่งที่มักลืมอย่างไฟล์แนบ ความเห็น บันทึกการตรวจสอบ และงานแบ็กกราวด์ด้วย

จากนั้นตั้งชื่อบทบาทที่ผู้ใช้ใช้จริง เก็บชุดบทบาทให้เล็กและเข้าใจง่าย: owner, manager, agent, read-only นี่คือบทบาททางธุรกิจที่คุณจะจับเข้ากับการตรวจสอบนโยบาย (ไม่ใช่บทบาทฐานข้อมูล)

แล้วตัดสินใจว่าแถวถูกเป็นเจ้าของอย่างไร บางตารางเป็นของผู้ใช้คนเดียว (เช่น โน้ตส่วนตัว) อื่น ๆ เป็นของทีม (เช่น inbox ร่วม) การผสมกันโดยไม่มีแผนจะทำให้นโยบายยากอ่านและง่ายจะหลุด

วิธีง่าย ๆ ในการเอกสารกฎของคุณคือให้ตอบคำถามเดิมสำหรับแต่ละตาราง:

  • ขอบเขตผู้เช่าคือคอลัมน์ไหน (คอลัมน์ใดบังคับมัน)?
  • ใครอ่านแถวได้ (ตามบทบาทและตามความเป็นเจ้าของ)?
  • ใครสร้างและอัปเดตแถวได้ (ภายใต้เงื่อนไขใด)?
  • ใครลบแถวได้ (มักจะเข้มงวดที่สุด)?
  • ข้อยกเว้นใดอนุญาตบ้าง (พนักงานซัพพอร์ต, ออโตเมชัน, การส่งออก)?

ตัวอย่าง: “Invoices” อาจอนุญาตให้ผู้จัดการดูใบแจ้งหนี้ทั้งหมดของผู้เช่า ตัวแทนดูใบแจ้งหนี้ของลูกค้าที่มอบหมายมา และผู้ชมอ่านอย่างเดียวดูได้แต่แก้ไขไม่ได้ ตัดสินล่วงหน้าว่ากฎใดต้องเข้มงวด (การแยกผู้เช่า, การลบ) และกฎใดยืดหยุ่นได้บ้าง (การมองเห็นเพิ่มเติมสำหรับผู้จัดการ) ถ้าคุณสร้างด้วยเครื่องมือ no-code เช่น AppMaster การทำแมปนี้ยังช่วยให้คาดหวัง UI และกฎฐานข้อมูลให้สอดคล้อง

แบบแผนการออกแบบสำหรับตารางมัลติเทนแนนท์

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

เริ่มจากเลือกตัวระบุผู้เช่าหนึ่งแบบและใช้มันทุกที่ UUID เป็นที่นิยมเพราะเดายากและสร้างได้ง่ายในหลายระบบ เลขจำนวนเต็มก็ใช้ได้โดยเฉพาะในแอปภายใน Slug (เช่น "acme") อ่านง่ายสำหรับคน แต่เปลี่ยนได้ จึงควรใช้เป็นฟิลด์แสดงผล ไม่ใช่คีย์หลัก

สำหรับข้อมูลที่ขอบเขตอยู่กับผู้เช่า ให้เพิ่มคอลัมน์ tenant_id ในทุกตารางที่เป็นของผู้เช่า และตั้งเป็น NOT NULL เมื่อเป็นไปได้ หากแถวสามารถมีอยู่โดยไม่มียูนิตผู้เช่า นั่นมักเป็นกลิ่นเหม็นของการผสมข้อมูลระดับโลกกับข้อมูลผู้เช่าในตารางเดียว ซึ่งทำให้นโยบาย RLS ยากและเปราะบาง

การทำดัชนีง่ายแต่สำคัญ คิวรีส่วนใหญ่ในแอป SaaS กรองโดย tenant ก่อน แล้วกรองตามฟิลด์ธุรกิจเช่น status หรือ date ค่าเริ่มต้นที่ดีคือดัชนีบน tenant_id และสำหรับตารางที่มีการใช้งานสูง ดัชนีแบบคอมโพสิตเช่น (tenant_id, created_at) หรือ (tenant_id, status) ตามฟิลเตอร์ที่ใช้บ่อย

ตัดสินใจแต่แรกว่าตารางใดเป็น global และตารางใดเป็น tenant-scoped ตาราง global ทั่วไปเช่น countries, currency codes, หรือ plan definitions ตาราง tenant-scoped ได้แก่ customers, invoices, tickets และสิ่งที่ผู้เช่าเป็นเจ้าของ

ถ้าคุณอยากให้ชุดกฎรักษาได้ ให้อยู่ให้แคบ:

  • ตารางที่มีขอบเขตผู้เช่า: tenant_id NOT NULL, เปิด RLS, นโยบายตรวจ tenant_id เสมอ
  • ตารางอ้างอิงระดับโลก: ไม่มี tenant_id, ไม่มีนโยบายผู้เช่า, อ่านได้สำหรับบทบาทส่วนใหญ่
  • ตารางร่วมแต่ควบคุมได้: แยกตารางตามคอนเซปต์ (หลีกเลี่ยงการผสมแถวระดับโลกและผู้เช่า)

ถ้าคุณสร้างด้วยเครื่องมืออย่าง AppMaster ความสอดคล้องนี้จะช่วยได้ เมื่อ tenant_id เป็นฟิลด์มาตรฐาน คุณสามารถนำรูปแบบเดียวกันไปใช้ซ้ำข้ามโมดูลโดยไม่เซอร์ไพรส์

ทีละขั้นตอน: สร้างนโยบายผู้เช่าแรกของคุณ

เริ่มจากพื้นฐานมัลติเทนแนนท์
เริ่มด้วยโครงสร้างมัลติเทนแนนท์ชัดเจนสำหรับ SaaS และพอร์ทัล แล้วปรับบทบาทและการเป็นเจ้าของข้อมูล
เริ่มจากเทมเพลต

ชัยชนะแรกที่ดีด้วย PostgreSQL RLS คือการเลือกตารางเดียวที่อ่านได้เฉพาะภายในผู้เช่าเท่านั้น จุดประสงค์ง่าย ๆ คือ: แม้ใครสักคนจะลืม WHERE ใน API ฐานข้อมูลก็จะปฏิเสธการคืนแถวจากผู้เช่าอื่น

เริ่มจากตารางที่มีคอลัมน์ tenant_id:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

เมื่อเปิด RLS พฤติกรรมเริ่มต้นมักทำให้คนประหลาดใจ: ถ้าบทบาทนั้นถูกบังคับด้วย RLS และไม่มีนโยบายที่ตรงกัน SELECT จะคืนแถวเป็นศูนย์ (และการเขียนจะล้มเหลว) นั่นคือสิ่งที่คุณต้องการตอนเริ่มต้น

ตอนนี้เพิ่มนโยบายอ่านแบบขั้นต่ำ ตัวอย่างนี้สมมติว่าแอปของคุณตั้งตัวแปรเซสชันเช่น app.tenant_id หลังการล็อกอิน:

CREATE POLICY invoices_tenant_read
ON invoices
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::uuid);

ต่อไปเพิ่มกฎการเขียน ใน RLS, USING ควบคุมว่าแถวเดิมใดที่คุณสามารถแตะต้องได้ และ WITH CHECK ควบคุมค่าที่คุณอนุญาตให้เขียนเป็นค่าใหม่

CREATE POLICY invoices_tenant_insert
ON invoices
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY invoices_tenant_update
ON invoices
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY invoices_tenant_delete
ON invoices
FOR DELETE
USING (tenant_id = current_setting('app.tenant_id')::uuid);

นโยบายเป็น PERMISSIVE โดยค่าเริ่มต้น หมายความว่านโยบายใดนโยบายหนึ่งสามารถอนุญาตการเข้าถึงได้ เลือก RESTRICTIVE เมื่อคุณต้องการกฎที่ต้องผ่านทั้งหมด (มีประโยชน์เมื่อเพิ่มการ์ดที่สองเช่น "บัญชีที่ใช้งานอยู่เท่านั้น")

เก็บนโยบายให้เล็กและมุ่งสู่บทบาท แทนที่จะเขียนกฎยักษ์กับ OR มากมาย ให้สร้างนโยบายแยกตามผู้ชม (เช่น invoices_tenant_read_app_user และ invoices_tenant_read_support_agent) จะทดสอบง่าย ตรวจทานง่าย และปลอดภัยต่อการเปลี่ยนในอนาคต

ส่งบริบทผู้เช่าและผู้ใช้อย่างปลอดภัย

เพื่อให้ PostgreSQL RLS ทำงาน ฐานข้อมูลต้องรู้ว่า "ใครเป็นคนเรียก" และ "พวกเขาเป็นผู้เช่าคนไหน" นโยบาย RLS เทียบแถวกับค่าที่ฐานข้อมูลอ่านได้เวลารันคิวรี ดังนั้นคุณต้องส่งบริบทนั้นเข้าสู่เซสชัน

รูปแบบทั่วไปคือการตั้งตัวแปรเซสชันหลังการพิสูจน์ตัวตน แล้วให้นโยบายอ่านค่าจาก current_setting() แอปพิสูจน์ตัวตน (เช่น โดยการยืนยัน JWT) แล้วคัดลอกเฉพาะฟิลด์ที่ต้องการ (tenant_id, user_id, role) ลงในการตั้งค่าการเชื่อมต่อฐานข้อมูล

-- Run once per request (or per transaction)
SELECT set_config('app.tenant_id', '3f2a0c3e-9c7b-4d3f-9c5c-3c5e9c5d1a11', true);
SELECT set_config('app.user_id',   '8d9c6b1a-6b6d-4e32-9c0d-2bfe6f6c1111', true);
SELECT set_config('app.role',      'support_agent', true);

-- In a policy
-- tenant_id column is a UUID
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);

การใช้อาร์กิวเมนต์ตัวที่สาม true ทำให้มันเป็น "local" ต่อทรานแซกชันปัจจุบัน นั่นสำคัญถ้าคุณใช้ connection pooling: การเชื่อมต่อที่ถูกพูลอาจถูกนำกลับมาใช้ซ้ำโดยคำขออื่น ดังนั้นคุณไม่ต้องการให้บริบทผู้เช่าของเมื่อวานค้างอยู่

เติมบริบทจาก JWT claims

ถ้า API ของคุณใช้ JWT ให้ถือ claims เป็นอินพุต ไม่ใช่ความจริง ตรวจสอบลายเซ็นและวันหมดอายุของโทเคนก่อน แล้วคัดลอกเฉพาะฟิลด์ที่ต้องการ (tenant_id, user_id, role) ลงในการตั้งค่าเซสชัน หลีกเลี่ยงการให้ไคลเอนต์ส่งค่าเหล่านี้โดยตรงเป็น header หรือ query param

บริบทที่ขาดหรือไม่ถูกต้อง: ปฏิเสธโดยค่าเริ่มต้น

ออกแบบนโยบายให้ค่าที่ขาดหายทำให้ไม่มีแถว

ใช้ current_setting('app.tenant_id', true) เพื่อให้ค่าที่ขาดกลับ NULL แปลงเป็นชนิดที่ถูกต้อง (เช่น ::uuid) เพื่อให้ฟอร์แมตที่ไม่ถูกต้องล้มเหลวเร็ว และยกเลิกคำขอถ้าตั้งบริบทผู้เช่าหรือผู้ใช้ไม่ได้ แทนที่จะเดาค่าเริ่มต้น

นี้ทำให้การควบคุมการเข้าถึงสอดคล้องแม้คำขอจะข้าม UI หรือเพิ่ม endpoint ใหม่ทีหลัง

แบบบทบาทที่ปฏิบัติได้และรักษาง่าย

เพิ่มฟีเจอร์โดยไม่เพิ่มความเสี่ยง
เพิ่มการชำระเงิน ข้อความ และการอัตโนมัติโดยไม่ทำลายขอบเขตของผู้เช่า
สำรวจการเชื่อมต่อ

วิธีที่ง่ายที่สุดในการรักษานโยบาย PostgreSQL RLS ให้อ่านง่ายคือแยกตัวตนออกจากสิทธิ์ พื้นฐานที่มั่นคงคือมีตาราง users และตาราง memberships ที่เชื่อมผู้ใช้กับผู้เช่าและบทบาท (หรือหลายบทบาท) แล้วนโยบายของคุณสามารถตอบคำถามเดียวได้ว่า: "ผู้ใช้ปัจจุบันมี membership ที่ถูกต้องสำหรับแถวนี้หรือไม่?"

เก็บชื่อบทบาทให้ผูกกับการกระทำจริง ไม่ใช่ชื่อตำแหน่งงาน “invoice_viewer” และ “invoice_approver” มักยืนยาวกว่าสำหรับนโยบายเพราะสามารถเขียนด้วยคำเรียบง่ายได้

นี่คือแพทเทิร์นบทบาทบางอย่างที่ยังคงเรียบง่ายเมื่อแอปเติบโต:

  • Owner-only: แถวมี created_by_user_id (หรือ owner_user_id) และการตรวจสอบคือการจับคู่ตรง
  • Team-only: แถวมี team_id และนโยบายตรวจว่าผู้ใช้เป็นสมาชิกทีมภายในผู้เช่าเดียวกัน
  • Approved-only: อ่านอนุญาตเมื่อ status = 'approved' และเขียนจำกัดแก่ผู้อนุมัติ
  • กฎผสม: เริ่มเข้มงวด แล้วเพิ่มข้อยกเว้นเล็ก ๆ (เช่น “support ดูได้ แต่เฉพาะภายในผู้เช่า”)

แอดมินข้ามผู้เช่าเป็นจุดที่หลายทีมพลาด จัดการกับพวกเขาอย่างชัดเจน อย่าใช้เป็นทางลัด "superuser" ที่ซ่อนอยู่ สร้างคอนเซปต์แยกเช่น platform_admin (ระดับโลก) และให้มีการตรวจสอบเฉพาะเจาะจงในนโยบาย จะดียิ่งขึ้นถ้าทำให้การเข้าถึงข้ามผู้เช่ามักเป็นแบบอ่านอย่างเดียวเป็นค่าเริ่มต้น และให้การเขียนต้องการบาร์สูงกว่า

การอธิบายนโยบายสำคัญกว่าที่คิด ใส่คอมเมนต์สั้น ๆ เหนือนโยบายแต่ละตัวที่อธิบายเจตนา ไม่ใช่ SQL “ผู้อนุมัติเปลี่ยนสถานะได้ ผู้ชมอ่านได้เฉพาะใบแจ้งหนี้ที่อนุมัติ” หกเดือนต่อมา โน้ตนั้นแหละที่จะช่วยให้การแก้ไขนโยบายปลอดภัย

ถ้าคุณสร้างด้วยเครื่องมือ no-code เช่น AppMaster รูปแบบเหล่านี้ยังใช้ได้ UI และ API ของคุณจะเคลื่อนไหวได้เร็ว แต่กฎในฐานข้อมูลยังคงมั่นคงเพราะพึ่งพา memberships และความหมายของบทบาทที่ชัดเจน

ตัวอย่างสถานการณ์: SaaS ง่าย ๆ กับใบแจ้งหนี้และซัพพอร์ต

รวมกฎธุรกิจไว้ที่เดียว
เปลี่ยนบทบาท ความเป็นเจ้าของ และการอนุมัติเป็นตรรกะฝั่งเซิร์ฟเวอร์โดยไม่ต้องเขียนระบบยืนยันสิทธิใหม่ทั้งแอป
สร้างโปรเจกต์

สมมติ SaaS เล็ก ๆ ให้บริการหลายบริษัท แต่ละบริษัทเป็นผู้เช่า แอปมีใบแจ้งหนี้ (เงิน) และตั๋วซัพพอร์ต (การช่วยเหลือลูกค้ารายวัน) ผู้ใช้เป็นได้ทั้ง agent, manager, หรือ support

โมเดลข้อมูล (ย่อ): ทุกแถวใบแจ้งหนี้และตั๋วมี tenant_id ตั๋วยังมี assignee_user_id ติดมาด้วย แอปตั้งผู้เช่าและผู้ใช้ปัจจุบันในเซสชันฐานข้อมูลทันทีหลังล็อกอิน

นี่คือวิธีที่ PostgreSQL RLS ลดความเสี่ยงในชีวิตประจำวัน

ผู้ใช้จาก Tenant A เปิดหน้าจอใบแจ้งหนี้และพยายามเดา ID ใบแจ้งหนี้จาก Tenant B (หรือ UI ส่งมันโดยบังเอิญ) คิวรียังคงรัน แต่ฐานข้อมูลคืนแถวเป็นศูนย์เพราะนโยบายต้องการ invoice.tenant_id = current_tenant_id ไม่มีการรั่วไหลเป็น "access denied" เพียงแค่ผลลัพธ์ว่าง

ภายในผู้เช่าเดียวกัน บทบาทจะจำกัดการเข้าถึงต่อไป ผู้จัดการเห็นใบแจ้งหนี้และตั๋วทั้งหมดของผู้เช่า ตัวแทนเห็นเฉพาะตั๋วที่มอบหมายให้เขา บางทีอาจเห็นแบบร่างของตนเอง นี่คือจุดที่ทีมมักพลาดใน API โดยเฉพาะเมื่อตัวกรองเป็นแบบเลือกได้

ซัพพอร์ตเป็นกรณีพิเศษ พวกเขาอาจต้องดูใบแจ้งหนี้เพื่อช่วยลูกค้า แต่ไม่ควรแก้ไขฟิลด์ที่ละเอียดอ่อนเช่น amount, bank_account, หรือ tax_id รูปแบบปฏิบัติได้คือ:

  • อนุญาต SELECT บน invoices สำหรับบทบาท support (ยังคงถูกจำกัดโดยผู้เช่า)
  • อนุญาต UPDATE เฉพาะผ่านทาง "เส้นทางที่ปลอดภัย" (เช่น view ที่เปิดเผยคอลัมน์ที่แก้ไขได้ หรือ policy อัปเดตที่เข้มงวดที่ปฏิเสธการเปลี่ยนแปลงฟิลด์ที่ป้องกัน)

ตอนนี้สถานการณ์บั๊ก API ที่เผลอลืมตัวกรอง: endpoint ลืมใส่ตัวกรองผู้เช่าในระหว่าง refactor หากไม่มี RLS มันอาจรั่วใบแจ้งหนี้ข้ามผู้เช่าได้ แต่ด้วย RLS ฐานข้อมูลจะปฏิเสธการคืนแถวภายนอกผู้เช่าเซสชัน จึงทำให้บั๊กเป็นเพียงหน้าจอเสีย ไม่ใช่การละเมิดข้อมูล

ถ้าคุณสร้าง SaaS แบบนี้ใน AppMaster คุณยังอยากให้กฎเหล่านี้อยู่ในฐานข้อมูล การตรวจสอบใน UI ช่วยได้ แต่กฎฐานข้อมูลคือน้ำหนักสุดท้ายเมื่อมีบางอย่างหลุด

ความผิดพลาดทั่วไปและวิธีหลีกเลี่ยง

RLS มีพลัง แต่ความสะเพร่าตัวเล็ก ๆ สามารถเปลี่ยนจาก "ปลอดภัย" เป็น "น่าประหลาดใจ" ปัญหาส่วนใหญ่เกิดเมื่อมีการเพิ่มตารางใหม่ บทบาทเปลี่ยน หรือใครสักคนทดสอบด้วยผู้ใช้ฐานข้อมูลไม่ถูกต้อง

ความล้มเหลวทั่วไปคือการลืมเปิด RLS บนตารางใหม่ คุณอาจเขียนนโยบายอย่างระมัดระวังสำหรับตารางหลัก แล้วเพิ่มตาราง "notes" หรือ "attachments" ในภายหลังแล้วปล่อยมันด้วยสิทธิ์เต็ม ทำให้เป็นนิสัย: ตารางใหม่ต้องเปิด RLS และมีนโยบายอย่างน้อยหนึ่งอัน

กับดักอีกอันคือการไม่ตรงกันของนโยบายข้ามการกระทำ นโยบายที่อนุญาต INSERT แต่บล็อก SELECT จะดูเหมือนว่า "ข้อมูลหาย" ทันทีหลังสร้าง ข้อกลับกันก็เจ็บปวด: ผู้ใช้สามารถอ่านแถวที่พวกเขาไม่สามารถสร้างได้ ดังนั้นพวกเขาจะหาทางแก้ใน UI คิดเป็น flow: "สร้างแล้วดู", "อัปเดตแล้วเปิดใหม่", "ลบแล้วลิสต์" เสมอ

ระวัง SECURITY DEFINER functions พวกมันรันด้วยสิทธิ์ของเจ้าของฟังก์ชัน ซึ่งสามารถข้าม RLS ได้ถ้าคุณไม่ระวัง ถ้าใช้ ให้เก็บฟังก์ชันให้เล็ก ตรวจสอบอินพุต และหลีกเลี่ยง dynamic SQL เว้นแต่จำเป็นจริง ๆ

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

เพื่อจับปัญหาเร็ว ให้การตรวจสอบเป็นเรื่องปฏิบัติได้:

  • ทดสอบโดยใช้บทบาทฐานข้อมูลเดียวกับที่แอป production ใช้ ไม่ใช่ผู้ใช้แอดมินส่วนตัว
  • เพิ่มการทดสอบเชิงลบต่อหนึ่งตาราง: ผู้ใช้จากผู้เช่าอื่นต้องเห็นศูนย์แถว
  • ยืนยันว่าสำหรับแต่ละตารางรองรับการกระทำที่คาดหวัง: SELECT, INSERT, UPDATE, DELETE
  • ทบทวนการใช้ SECURITY DEFINER และอธิบายเหตุผล
  • ใส่ "RLS enabled?" ในเช็คลิสต์การรีวิวโค้ดและมิเกรชัน

ตัวอย่าง: ถ้าตัวแทนซัพพอร์ตสร้างโน้ตในใบแจ้งหนี้แต่ดูไม่ได้ มักเกิดจากนโยบาย INSERT โดยไม่มี SELECT ที่ตรงกัน (หรือบริบทผู้เช่าไม่ได้ถูกตั้งสำหรับเซสชันนั้น)

เช็คลิสต์ด่วนเพื่อยืนยันการตั้งค่า RLS ของคุณ

สร้างเครื่องมือภายในที่ปลอดภัย
สร้างหน้าจอการจัดการ ใบแจ้งหนี้ และตั๋วด้วยเกราะที่สอดคล้องกับกฎในฐานข้อมูลของคุณ
ลองเลย

RLS อาจดูถูกต้องในการรีวิวแต่ล้มเหลวในการใช้งานจริง การยืนยันไม่ใช่การอ่านนโยบาย แต่เป็นการพยายามเจาะมันด้วยบัญชีและคิวรีแบบสมจริง ทดสอบแบบที่แอปจะใช้ ไม่ใช่แบบที่คุณหวังว่ามันจะทำงาน

สร้างชุดตัวตนทดสอบเล็ก ๆ ก่อน ใช้อย่างน้อยสองผู้เช่า (Tenant A และ Tenant B) สำหรับแต่ละผู้เช่า ให้เพิ่มผู้ใช้ปกติและแอดมิน/ผู้จัดการ ถ้าคุณรองรับบทบาท "support agent" หรือ "read-only" ให้เพิ่มบทบาทนั้นด้วย

แล้วทดสอบ RLS ด้วยชุดการตรวจสอบซ้ำได้:

  • รันการกระทำหลักสำหรับแต่ละบทบาท: ลิสต์แถว ดึงแถวเดี่ยวตาม id, insert, update, delete สำหรับแต่ละการกระทำ ให้ลองกรณีที่ "อนุญาต" และ "ควรถูกบล็อก"
  • พิสูจน์ขอบเขตผู้เช่า: ในฐานะ Tenant A พยายามอ่านหรือแก้ไขข้อมูลของ Tenant B โดยใช้ id ที่รู้ว่ามี ผลต้องเป็นศูนย์หรือข้อผิดพลาดสิทธิ์ ไม่เคยเป็น "บางแถว"
  • ทดสอบ join เพื่อหาการรั่ว: join ตารางที่ป้องกันเข้ากับตารางอื่น (รวมถึง lookup tables) ยืนยันว่า join ไม่สามารถดึงแถวของผู้เช่าอื่นผ่าน foreign key หรือ view ได้
  • ตรวจสอบว่าการขาดหรือบริบทผิดปฏิเสธการเข้าถึง: ล้างบริบท tenant/user และลองอีกครั้ง "ไม่มีบริบท" ควรล้มเหลวแบบปิด นอกจากนี้ลอง tenant id ที่ไม่ถูกต้อง
  • ยืนยันประสิทธิภาพพื้นฐาน: ดูแผนคิวรีและตรวจว่าดัชนีรองรับรูปแบบการกรองตามผู้เช่า (โดยทั่วไป tenant_id บวกสิ่งที่คุณเรียงหรือตาม)

ถ้าการทดสอบใดทำให้ประหลาดใจ แก้นโยบายหรือการตั้งบริบทก่อน อย่าแพตช์ที่ UI หรือ API แล้วหวังว่ากฎฐานข้อมูลจะ "ค้ำไว้"

ขั้นตอนถัดไป: เปิดใช้งานอย่างปลอดภัยและรักษาความสม่ำเสมอ

ปฏิบัติต่อ PostgreSQL RLS เหมือนระบบความปลอดภัย: แนะนำอย่างรอบคอบ ยืนยันบ่อย ๆ และเก็บกฎให้เรียบง่ายพอที่ทีมจะปฏิบัติตาม

เริ่มเล็ก เลือกตารางที่การรั่วไหลจะทำให้เกิดความเสียหายมากที่สุด (payments, invoices, ข้อมูลบุคคลที่เป็นข้อมูลส่วนตัว, ข้อความลูกค้า) และเปิด RLS ที่นั่นก่อน ความสำเร็จเล็ก ๆ ดีกว่าการเปิดใช้งานครั้งใหญ่ที่ไม่มีใครเข้าใจทั้งหมด

ลำดับการเปิดใช้งานที่ปฏิบัติได้มักเป็นดังนี้:

  • ตารางหลักที่มีเจ้าของชัดเจนก่อน (แถวเป็นของผู้เช่าชัดเจน)
  • ตารางที่มีข้อมูลส่วนบุคคล (PII)
  • ตารางที่แชร์แต่กรองตามผู้เช่า (reports, analytics)
  • ตารางเชื่อมโยงและกรณีขอบ (many-to-many)
  • ทุกอย่างที่เหลือเมื่อพื้นฐานมั่นคง

ทำให้การทดสอบไม่ใช่ตัวเลือก ทดสอบอัตโนมัติควรรันคิวรีเดียวกันในฐานะผู้เช่าและบทบาทต่าง ๆ และยืนยันว่าผลลัพธ์เปลี่ยนตามที่คาด รวมทั้งเช็คทั้ง "ควรอนุญาต" และ "ควรถูกปฏิเสธ" เพราะบั๊กที่แพงที่สุดคือการให้สิทธิ์เกินโดยเงียบ

เก็บจุดเดียวชัดเจนใน flow ของคำขอที่ตั้งบริบทเซสชันก่อนคำถามใด ๆ tenant id, user id, และ role ควรถูกตั้งครั้งเดียว ตั้งแต่ต้น และไม่คาดเดาภายหลัง ถ้าคุณตั้งบริบทกึ่งกลางทรานแซกชัน สุดท้ายจะมีคิวรีที่ทำงานด้วยค่าที่ขาดหรือเก่า

เมื่อคุณสร้างด้วย AppMaster วางแผนให้ UI ที่สร้างโดยระบบและนโยบาย PostgreSQL สอดคล้องกัน มาตรฐานการส่งบริบทผู้เช่าและบทบาทไปยังฐานข้อมูล (เช่น ตัวแปรเซสชันเดียวกันสำหรับทุก endpoint) จะทำให้นโยบายทำงานเหมือนกันทุกที่ ถ้าคุณใช้ AppMaster ที่ appmaster.io, RLS ยังคงคุ้มค่าที่จะถือเป็นตัวตัดสินสุดท้ายสำหรับการแยกผู้เช่า แม้ว่าคุณจะควบคุมการเข้าถึงใน UI ด้วยก็ตาม

สุดท้าย จงสังเกตสิ่งที่ล้มเหลว การปฏิเสธการอนุญาตเป็นสัญญาณมีประโยชน์ โดยเฉพาะหลังการเปิดตัว ติดตามการถูกปฏิเสธซ้ำ ๆ และตรวจสอบว่ามันเป็นการโจมตีจริง ปัญหาของไคลเอนต์ หรือกฎที่เข้มงวดเกินไป

รายการนิสัยสั้น ๆ ที่ช่วยให้ RLS อยู่ได้:

  • มุมมองเริ่มจากปฏิเสธโดยค่าเริ่มต้น และเพิ่มข้อยกเว้นอย่างตั้งใจ
  • ชื่อนโยบายชัดเจน (ตาราง + การกระทำ + ผู้ชม)
  • การเปลี่ยนนโยบายต้องรีวิวเหมือนการเปลี่ยนโค้ด
  • บันทึกการปฏิเสธและรีวิวช่วงเปิดตัว
  • ชุดทดสอบเล็ก ๆ เพิ่มสำหรับแต่ละตารางที่เปิด RLS
ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม