فیلتری که برنامه نمیتواند فراموش کند
کارخانه نمایندگیهایی در تهران، مشهد و تبریز دارد و هر نمایندگی باید فقط مشتریها و سفارشهای خودش را ببیند. راه معمول، اضافه کردن WHERE branch_id = ... به همهی کوئریهای برنامه است؛ کافی است یک گزارش جدید آن را فراموش کند تا دادهی همهی نمایندگیها نشت کند. Row Level Security این شرط را به خود جدول میسپارد.
ALTER TABLE factory.orders ADD COLUMN branch_id int NOT NULL DEFAULT 1;
ALTER TABLE factory.orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY branch_isolation ON factory.orders
FOR ALL
TO carpet_rw
USING (branch_id = current_setting('app.branch_id')::int)
WITH CHECK (branch_id = current_setting('app.branch_id')::int);
-- نقش ستاد مرکزی همه را میبیند
CREATE POLICY hq_all ON factory.orders
FOR SELECT TO carpet_ro
USING (true);
USING تعیین میکند کدام ردیفها دیده شوند (SELECT، UPDATE، DELETE) و WITH CHECK کدام ردیفها نوشته شوند (INSERT و نتیجهی UPDATE). بدون WITH CHECK، نمایندگی تهران میتواند سفارشی با branch_id مشهد درج کند.
تنظیم شناسه در هر درخواست
BEGIN;
SET LOCAL app.branch_id = '3';
SELECT count(*) FROM factory.orders; -- فقط سفارشهای نمایندگی ۳
COMMIT;
با SET LOCAL مقدار در پایان تراکنش پاک میشود؛ این برای PgBouncer در حالت transaction pooling حیاتی است، چون اتصال بعدی ممکن است متعلق به نمایندگی دیگری باشد. در جنگو میتوانید در یک middleware داخل transaction.atomic() این SET LOCAL را اجرا کنید.
چه کسی RLS را دور میزند
| نقش | RLS اعمال میشود؟ |
|---|---|
| superuser | هرگز |
| role با ویژگی BYPASSRLS | خیر |
| مالک جدول | خیر، مگر FORCE ROW LEVEL SECURITY |
| بقیه بدون هیچ policy | هیچ ردیفی نمیبینند (پیشفرض بسته) |
کارایی
شرط policy مثل یک WHERE به کوئری اضافه میشود؛ پس روی branch_id ایندکس بگذارید یا آن را ستون اول ایندکسهای ترکیبی کنید. اگر policy تابعی صدا میزند، آن را داخل زیرکوئری بنویسید تا یک بار برای کل کوئری محاسبه شود، نه برای هر ردیف:
-- با فرض ستون branch_id در customers
CREATE POLICY branch_isolation_fast ON factory.customers
TO carpet_rw
USING (branch_id = (SELECT current_setting('app.branch_id')::int));
نکتههایی که کمتر کسی میداند
- اگر
app.branch_idتنظیم نشده باشد،current_setting('app.branch_id')خطا میدهد؛ با آرگومان دومtrueبهجای خطا NULL برمیگرداند و هیچ ردیفی دیده نمیشود. خطا دادن معمولاً امنتر است چون باگ را آشکار میکند. - policyهای چندگانهی PERMISSIVE با OR ترکیب میشوند؛ با
AS RESTRICTIVEشرطی میسازید که با AND اضافه شود، مثلاً «و فقط سفارشهای حذفنشده». - بررسی FK و UNIQUE از RLS عبور میکند؛ کاربر با درج یک کد تکراری و دیدن خطای duplicate key میتواند وجود ردیفی را که نمیبیند تشخیص دهد.
- pg_dump با کاربری که مشمول RLS است، بهطور پیشفرض خطا میدهد تا بکاپ ناقص نگیرد؛ بکاپ را با مالک یا نقش BYPASSRLS بگیرید.
- view در حالت پیشفرض با مجوز صاحبش اجرا میشود و اگر صاحبش مالک جدول باشد، RLS را دور میزند؛ برای viewهای روی جدول RLSدار
security_invoker = trueبگذارید.