فصل ۷: مدیریت، امنیت و دسترس‌پذیری

Row Level Security: هر نمایندگی فقط سفارش‌های خودش

فیلتری که برنامه نمی‌تواند فراموش کند

کارخانه نمایندگی‌هایی در تهران، مشهد و تبریز دارد و هر نمایندگی باید فقط مشتری‌ها و سفارش‌های خودش را ببیند. راه معمول، اضافه کردن 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 بگذارید.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.