أنماط RLS في PostgreSQL لتطبيقات متعددة المستأجرين
تعلم أساسيات PostgreSQL RLS وأنماط عزل المستأجرين والأدوار لتطبيق قواعد الوصول في قاعدة البيانات بدلاً من الاعتماد فقط على التطبيق.

لماذا يهم تطبيق التحكم في الوصول على مستوى قاعدة البيانات في تطبيقات الأعمال
تحتوي تطبيقات الأعمال عادةً على قواعد مثل “المستخدمون لا يمكنهم رؤية سجلات شركة أخرى” و"فقط المديرون يمكنهم الموافقة على رد الأموال". كثير من الفرق تفرض تلك القواعد في واجهة المستخدم أو في الـ API وتفترض أن ذلك يكفي. المشكلة هي أن أي مسار إضافي إلى قاعدة البيانات يصبح فرصة لتسريب البيانات: أداة إدارة داخلية، مهمة خلفية، استعلام تحليلي، نهاية نقطة منسية، أو خطأ يتخطى أحد الفحوصات.
عزل المستأجر يعني أن عميلًا واحدًا (مستأجر) لا يمكنه أبدًا قراءة أو تغيير بيانات عميل آخر، حتى عن طريق الخطأ. الوصول القائم على الأدوار يعني أن الأشخاص داخل نفس المستأجر لا يزال لديهم صلاحيات مختلفة، مثل الوكلاء مقابل المديرين مقابل فريق المالية. هذه القواعد سهلة الوصف، لكنها صعبة الحفاظ على اتساقها عندما تكون موزعة في أماكن متعددة.
PostgreSQL row-level security (RLS) هي ميزة في قاعدة البيانات تسمح للقاعدة بتحديد أي الصفوف يمكن للطلب رؤيتها أو تعديلها. بدلًا من الاعتماد على أن كل استعلام في تطبيقك يتذكر جملة WHERE الصحيحة، تطبق قاعدة البيانات السياسات تلقائيًا.
RLS ليست درعًا سحريًا لكل شيء. لن تصمم مخططك، ولن تحل محل المصادقة، ولن تحميك من شخص يمتلك صلاحية قاعدة بيانات قوية (مثل superuser). كما أنها لن تمنع أخطاء منطقية مثل "شخص يمكنه تحديث صف لا يمكنه تحديده" ما لم تكتب سياسات لكل من القراءة والكتابة.
ما تحصل عليه هو شبكة أمان قوية:
- مجموعة واحدة من القواعد لجميع مسارات الكود التي تصل إلى القاعدة
- عدد أقل من لحظات "أوه" عند إطلاق ميزة جديدة
- تدقيقات أوضح، لأن قواعد الوصول مرئية في SQL
- دفاع أفضل إذا تسلل خطأ في الـ API
هناك تكلفة إعداد صغيرة. تحتاج طريقة متسقة لتمرير "من هو هذا المستخدم" و"أي مستأجر" إلى قاعدة البيانات، وتحتاج لصيانة السياسات أثناء نمو التطبيق. العائد كبير، خاصة لتطبيقات SaaS والأدوات الداخلية حيث تكون بيانات العملاء الحساسة على المحك.
أساسيات Row-Level Security بدون المصطلحات المعقدة
Row-Level Security (RLS) يصف تلقائيًا أي الصفوف يمكن للاستعلام رؤيتها أو تعديلها. بدل الاعتماد على أن كل شاشة أو نقطة نهاية أو تقرير "يتذكر" القواعد، تطبقها قاعدة البيانات نيابةً عنك.
مع PostgreSQL row-level security، تكتب سياسات تُفحص على كل SELECT وINSERT وUPDATE وDELETE. إذا قالت السياسة "يمكن لهذا المستخدم رؤية الصفوف للمستأجر A فقط"، فحتى صفحة المشرف المنسية أو استعلام تحليلي جديد أو تصحيح متسرع ستحصل على نفس الحواجز.
RLS يختلف عن GRANT/REVOKE. GRANT يحدد ما إذا كان لدور ما الحق في الوصول إلى جدول أصلاً (أو أعمدة محددة). RLS يحدد أي صفوف داخل ذلك الجدول مسموح بها. عمليًا، غالبًا ما تستخدم كلاهما: GRANT لتقليل من يمكنه الوصول إلى الجدول، وRLS لتقليل ما يمكنهم الوصول إليه داخل الجدول.
كما أنه يصمد في العالم الفوضوي. العروض (views) عادةً تحترم RLS لأن الوصول إلى الجدول الأساسي لا يزال يفعل السياسة. الانضمامات والاستعلامات الفرعية لا تزال تُفلتر، فلا يمكن للمستخدم "الانضمام" للوصول إلى بيانات الآخرين. وتطبق السياسة بغض النظر عن أي عميل يشغل الاستعلام: كود التطبيق، وحدة تحكم SQL، مهمة خلفية، أو أداة تقارير.
RLS مناسب عندما تحتاج إلى عزل قوي للمستأجرين، أو يوجد طرق متعددة للاستعلام عن نفس البيانات، أو أدوار عديدة تشترك في الجداول (كما في SaaS والأدوات الداخلية). قد يكون مبالغًا فيه لتطبيقات صغيرة بثلاثة طبقات وواجهة خلفية موثوقة واحدة، أو للبيانات غير الحساسة التي لا تغادر خدمة محكومة واحدة. بمجرد أن يكون لديك أكثر من نقطة دخول واحدة (أدوات إدارة، تصديرات، BI، نصوص)، عادةً ما يكون RLS مربحًا.
ابدأ بتخطيط المستأجرين والأدوار وملكية البيانات
قبل كتابة أي سياسة، وضّح من يملك ماذا. يعمل PostgreSQL RLS بشكل أفضل عندما يعكس نموذج البيانات بالفعل المستأجرين والأدوار والملكية.
ابدأ بالمستأجرين. في معظم تطبيقات SaaS، القاعدة البسيطة هي: كل جدول مشترك يحتوي بيانات العملاء له عمود tenant_id. يشمل ذلك الجداول الواضحة مثل الفواتير، وأيضًا الأشياء التي ينسى الناس غالبًا، مثل المرفقات والتعليقات وسجلات التدقيق والمهام الخلفية.
بعد ذلك، سمِّ الأدوار التي يستخدمها الناس فعليًا. اجعل المجموعة صغيرة وبسيطة: owner، manager، agent، read-only. هذه أدوار عمل سترتبط لاحقًا بفحوصات السياسات (ليست نفس أدوار قاعدة البيانات).
ثم قرّر كيف تُنسب السجلات. بعض الجداول مملوكة لمستخدم واحد (مثل ملاحظة خاصة). أخرى مملوكة لفريق (مثل صندوق وصول مشترك). المزج بين النوعين دون خطة يؤدي إلى سياسات صعبة القراءة وسهلة التجاوز.
طريقة بسيطة لتوثيق قواعدك هي الإجابة على نفس الأسئلة لكل جدول:
- ما هو حدود المستأجر (أي عمود يطبقه)؟
- من يمكنه قراءة الصفوف (حسب الدور والملكية)؟
- من يمكنه إنشاء وتحديث الصفوف (وتحت أي شروط)؟
- من يمكنه حذف الصفوف (عادةً أصعب قاعدة)؟
- ما الاستثناءات المسموح بها (طاقم الدعم، الأتمتة، التصديرات)؟
مثال: قد تسمح "الفواتير" للمديرين بعرض كل فواتير المستأجر، والوكلاء بعرض فواتير العملاء المعينة لهم، والمستخدمين للقراءة فقط بعرض دون تعديل. قرّر مسبقًا أي القواعد يجب أن تكون صارمة (عزل المستأجر، الحذف) وأيها يمكن أن تكون مرنة (وصول إضافي للمديرين). إذا بنت في أداة بدون كود مثل AppMaster، يساعد هذا التخطيط أيضًا في مزامنة توقعات الواجهة وقواعد قاعدة البيانات.
أنماط تصميم للجداول متعددة المستأجرين
يعمل RLS متعدد المستأجرين بشكل أفضل عندما تبدو جداولك متوقعة. إذا كان كل جدول يخزن المستأجر بطريقة مختلفة، تتحول سياساتك إلى أحجية. شكل متناسق يجعل PostgreSQL RLS أسهل قراءةً واختبارًا والحفاظ عليه مع الزمن.
ابدأ باختيار معرف مستأجر واحد واستخدمه في كل مكان. UUIDs شائعة لأنها صعبة التخمين وسهلة الإنشاء في أنظمة متعددة. الأعداد الصحيحة جيدة أيضًا، خاصة للتطبيقات الداخلية. الـ slugs (مثل "acme") سهلة القراءة للبشر، لكنها قد تتغير، فاعتبرها حقل عرض وليس المفتاح الأساسي.
لبيانات نطاق المستأجر، أضف عمود tenant_id لكل جدول ينتمي لمستأجر، واجعله NOT NULL متى أمكن. إذا كان يمكن أن يوجد صف بدون مستأجر، فعادة ما يكون ذلك إشارة رائحة: غالبًا ما تجمع بين بيانات عالمية وبيانات مستأجر في جدول واحد، مما يجعل سياسات RLS أصعب وأكثر هشاشة.
الفهرسة بسيطة لكنها مهمة. معظم الاستعلامات في تطبيق SaaS ترشّح حسب المستأجر أولًا، ثم حسب حقل عمل مثل الحالة أو التاريخ. الافتراضي الجيد هو فهرس على tenant_id، وللجداول ذات الحركة العالية فهرس مركب مثل (tenant_id, created_at) أو (tenant_id, status) حسب عوامل التصفية الشائعة.
قرّر مبكرًا أي الجداول عالمية وأيها نطاق مستأجر. جداول مرجعية شائعة عالمية تشمل البلدان، رموز العملات، أو تعريفات الخطط. الجداول بنطاق المستأجر تشمل العملاء، الفواتير، التذاكر، وأي شيء يملكه المستأجر.
إذا أردت مجموعة قواعد قابلة للصيانة، اجعلها ضيقة:
- جداول نطاق المستأجر:
tenant_id NOT NULL، RLS مفعّل، السياسات دائمًا تفحصtenant_id. - جداول مرجعية عالمية: لا
tenant_id، لا سياسات مستأجر، قراءة فقط لمعظم الأدوار. - جداول مشتركة لكن مسيطرة: فصّل الجداول حسب المفهوم (تجنّب المزج بين الصفوف العالمية والمستأجرة).
إذا بنيت بأداة مثل AppMaster، يفيد هذا الاتساق في نموذج البيانات أيضًا. بمجرد أن يصبح tenant_id حقلًا معياريًا، يمكنك إعادة استخدام نفس الأنماط عبر الوحدات دون مفاجآت.
خطوة بخطوة: أنشئ أول سياسة مستأجر لديك
فوز بسيط أولي مع 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 عندما تريد قواعد يجب أن تمرّ كلها (مفيد لإضافة حارس ثانٍ مثل "الحسابات النشطة فقط").
اجعل السياسات صغيرة ومركزة على الدور. بدلًا من قاعدة عملاقة تحتوي على الكثير من ORs، أنشئ سياسات منفصلة لكل جمهور (مثل 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 يجعل الإعداد "محليًا" للمعاملة الحالية. هذا مهم إذا كنت تستخدم تجميع اتصالات (connection pooling): قد تُعاد استخدام اتصال مجمّع لطلب آخر، فلا تريد سياق مستأجر البارحة يبقى عالقًا.
تعبئة السياق من مطالبات JWT
إذا كان الـ API يستخدم JWTs، عامل المطالبات كمدخلات، لا كحقيقة مطلقة. تحقق من توقيع التوكن وانتهائه أولًا، ثم انسخ فقط الحقول التي تحتاجها (tenant_id، user_id، role) إلى إعدادات الجلسة. تجنب السماح للعملاء بإرسال هذه القيم مباشرة كرؤوس أو معاملات.
سياق مفقود أو غير صالح: الرفض افتراضيًا
صمّم السياسات بحيث أن الإعدادات المفقودة تؤدي إلى عدم إرجاع صفوف.
استخدم current_setting('app.tenant_id', true) حتى تُرجع القيم المفقودة NULL. قم بالتحويل إلى النوع الصحيح (مثل ::uuid) حتى تفشل التنسيقات غير الصالحة بسرعة. وافرض فشل الطلب إذا تعذر ضبط سياق المستأجر/المستخدم، بدلًا من تخمين قيمة افتراضية.
هذا يبقي التحكم في الوصول متسقًا حتى عندما يتخطى استعلام واجهة المستخدم أو تُضاف نقطة نهاية جديدة لاحقًا.
أنماط أدوار عملية قابلة للصيانة
أسهل طريقة للحفاظ على سياسات RLS قابلة للقراءة هي فصل الهوية عن الأذونات. قاعدة صلبة هي وجود جدول users بالإضافة إلى جدول memberships يربط المستخدم بالمستأجر والدور (أو عدة أدوار). ثم يمكن لسياساتك الإجابة على سؤال واحد: "هل لدى المستخدم الحالي العضوية المناسبة لهذا الصف؟"
اجعل أسماء الأدوار مرتبطة بأفعال حقيقية، لا بألقاب وظيفية. invoice_viewer وinvoice_approver عادةً ما تدومان أفضل من "manager"، لأن السياسة يمكن كتابتها بكلمات واضحة.
فيما يلي بعض أنماط الأدوار التي تبقى بسيطة مع نمو التطبيق:
- مالك فقط: الصف له
created_by_user_id(أوowner_user_id) وتفحص الوصول تطابقه بالضبط. - فريق فقط: الصف له
team_id، والسياسة تفحص أن المستخدم عضو في ذلك الفريق داخل نفس المستأجر. - مصدق فقط: تُسمح القراءات فقط عندما
status = 'approved'، والكتابات مقتصرة على المصادقين. - قواعد مختلطة: ابدأ صارمًا، ثم أضف استثناءات صغيرة (مثال: "الدعم يمكنه القراءة، لكن فقط داخل المستأجر").
المسؤولون عبر المستأجرين هم حيث تقع العديد من الفرق في المشاكل. عالجهم صراحة، لا كاختصار "superuser" مخفي. أنشئ مفهومًا منفصلاً مثل platform_admin (عالمي) واطلب فحصًا متعمدًا في السياسة. ومن الأفضل إبقاء الوصول عبر المستأجرين في حالة قراءة فقط افتراضيًا واجعل الكتابة تتطلب مستوى أعلى.
التوثيق أهم مما يبدو. ضع تعليقًا قصيرًا فوق كل سياسة يشرح النية، لا SQL فقط. "المصادقون يمكنهم تغيير الحالة. المشاهدون يمكنهم قراءة الفواتير المعتمدة فقط." بعد ستة أشهر، ذلك السطر هو ما يحافظ على سلامة تعديلات السياسة.
إذا بنيت بأداة بدون كود مثل AppMaster، تنطبق هذه الأنماط أيضًا. يمكن لواجهة المستخدم وواجهة الـ API أن تتحرك بسرعة، لكن قواعد القاعدة تبقى ثابتة لأنها تعتمد على العضويات ومعنى الأدوار الواضح.
سيناريو مثال: SaaS بسيط بالفواتير والدعم
تخيل SaaS صغير يخدم عدة شركات. كل شركة هي مستأجر. التطبيق يحتوي فواتير (مال) وتذاكر دعم (مساعدة يومية). المستخدمون قد يكونون وكلاء أو مديرين أو دعم.
نموذج البيانات (مبسط): كل صف فاتورة وتذكرة له tenant_id. التذاكر أيضًا لها assignee_user_id. يضبط التطبيق المستأجر والمستخدم في جلسة قاعدة البيانات مباشرة بعد تسجيل الدخول.
إليك كيف يغير PostgreSQL RLS مخاطرة اليوم إلى اليوم.
مستخدم من المستأجر A يفتح شاشة الفواتير ويحاول تخمين معرف فاتورة من المستأجر B (أو أرسلت الواجهة معرف خاطئ بالخطأ). الاستعلام ما زال يُشغّل، لكن القاعدة ترجع صفر صفوف لأن السياسة تتطلب invoice.tenant_id = current_tenant_id. لا يوجد تسريب "تم رفض الوصول"، فقط نتيجة فارغة.
داخل مستأجر واحد، تضيق الأدوار الوصول أكثر. المدير يمكنه رؤية كل الفواتير وكل التذاكر لمستأجره. الوكيل يمكنه رؤية التذاكر المعينة له فقط، وربما مسوداته الخاصة. هنا غالبًا ما تخطئ الفرق في الـ API، خاصةً عندما تكون عوامل التصفية اختيارية.
الدعم حالة خاصة. قد يحتاجون إلى عرض الفواتير لمساعدة العملاء، لكن لا ينبغي لهم تغيير حقول حساسة مثل amount أو bank_account أو tax_id. نمط عملي هو:
- السماح بـ
SELECTعلى الفواتير لدور الدعم (ما زالت مقيدة بالمستأجر). - السماح بـ
UPDATEفقط عبر مسار "آمن" (مثل view يعرض أعمدة قابلة للتعديل، أو سياسة تحديث صارمة ترفض تغييرات الحقول المحمية).
الآن سيناريو "خطأ API عرضي": نقطة نهاية نسيت تطبيق فلتر المستأجر أثناء إعادة هيكلة. بدون RLS، يمكن أن يتسرب عبرها فواتير بين المستأجرين. مع RLS، ترفض القاعدة إرجاع الصفوف خارج مستأجر الجلسة، فيصبح الخطأ شاشة معطلة بدلًا من خرق بيانات.
إذا بنيت هذا النوع من SaaS في AppMaster، لا يزال من الحكمة وضع هذه القواعد في القاعدة. فحوص الواجهة مفيدة، لكن قواعد القاعدة هي ما يصمد عندما ينزلق شيء.
أخطاء شائعة وكيف تتجنبها
PostgreSQL RLS قوي، لكن سقطات صغيرة يمكن أن تحول "آمن" إلى "مفاجئ" بهدوء. تظهر معظم المشاكل عند إضافة جدول جديد، تغيير دور، أو اختبار بمستخدم قاعدة بيانات خاطئ.
فشل شائع هو نسيان تفعيل RLS على جدول جديد. قد تكتب سياسات بعناية لجدولك الأساسي، ثم تضيف جدول "ملاحظات" أو "مرفقات" لاحقًا وتطلقه بكامل الصلاحية. اجعلها عادة: جدول جديد يعني RLS مفعّل، ومعه سياسة واحدة على الأقل.
فخ شائع آخر هو سياسات غير متطابقة عبر الإجراءات. سياسة تسمح INSERT لكن تحجب SELECT يمكن أن تبدو كأن "البيانات تختفي" فور إنشائها. والعكس مؤلم أيضًا: المستخدمون يمكنهم قراءة صفوف لا يستطيعون إنشاؤها، فيلتفون حولها في الواجهة. فكّر في التدفقات: "إنشاء ثم عرض"، "تحديث ثم إعادة فتح"، "حذف ثم قائمة".
كن حذرًا مع الدوال SECURITY DEFINER. تعمل بامتيازات مالك الدالة، والذي قد يتجاوز RLS إذا لم تكن صارمًا. إذا استخدمت هذه الدوال، اجعلها صغيرة، تحقق من المدخلات، وتجنب SQL الديناميكي ما لم تكن بحاجة فعلًا.
أيضًا تجنب الاعتماد على تصفية جانب التطبيق مع ترك وصول قاعدة البيانات مفتوحًا. حتى الـ APIs المصممة جيدًا تنمو وتضيف نقاط نهاية ومهام خلفية ونصوص إدارة. إذا كان دور قاعدة البيانات يمكنه قراءة كل شيء، عاجلًا أم آجلًا سيحدث شيء.
لالتقاط المشاكل مبكرًا، اجعل الفحوص عملية:
- اختبر باستخدام نفس دور قاعدة البيانات الذي يستخدمه تطبيق الإنتاج، لا مستخدم المشرف الشخصي.
- أضف اختبار سلبي واحد على الأقل لكل جدول: يجب أن يرى مستخدم من مستأجر آخر صفر صفوف.
- تأكد أن كل جدول يدعم الإجراءات التي تتوقعها:
SELECT،INSERT،UPDATE،DELETE. - راجع استخدام
SECURITY DEFINERووثق سبب الحاجة إليه. - أدرج "هل RLS مفعّل؟" في قوائم مراجعة الكود والـ migrations.
مثال: إذا أنشأ وكيل دعم ملاحظة على فاتورة لكنه لا يستطيع قراءتها لاحقًا، غالبًا ما يكون ذلك سياسة INSERT دون سياسة SELECT مطابقة (أو أن سياق المستأجر لم يُضبط لتلك الجلسة).
قائمة تحقق سريعة للتحقق من إعداد RLS
قد يبدو RLS صحيحًا في المراجعة ويظل يفشل في الاستخدام الحقيقي. التحقق أقل عن قراءة السياسات وأكثر عن محاولة كسرها بحسابات واستعلامات واقعية. اختبرها بالطريقة التي سيستخدمها تطبيقك، لا بالطريقة التي تأمل أن تعمل بها.
أنشئ مجموعة صغيرة من الهويات للاختبار أولًا. استخدم على الأقل مستأجرين (مستأجر A ومستأجر B). لكل مستأجر، أضف مستخدمًا عاديًا ودور مشرف أو مدير. إن كنت تدعم "وكيل دعم" أو "قراءة فقط"، أضف واحدًا منهم أيضًا.
ثم اختبر RLS بقائمة اختبارات قصيرة وقابلة للتكرار:
- شغّل العمليات الأساسية لكل دور: سرد الصفوف، جلب صف واحد بالمعرّف، إدراج، تحديث، حذف. لكل عملية، جرّب حالات "مسموح" و"يجب حجبها".
- إثبات حدود المستأجر: كمستأجر A، حاول قراءة أو تعديل بيانات مستأجر B باستخدام معرّفات تعرف أنها موجودة. يجب أن تحصل على صفر صفوف أو خطأ صلاحية، وليس "بعض الصفوف".
- اختبر الانضمامات للكشف: انضم جداول محمية إلى جداول أخرى (بما في ذلك جداول lookup). تأكد من أن الانضمام لا يمكنه جلب صفوف مرتبطة من مستأجر آخر عبر مفتاح خارجي أو view.
- تحقق من أن السياق المفقود أو الخاطئ يرفض الوصول: امسح سياق المستأجر/المستخدم وأعد المحاولة. "بدون سياق" يجب أن يفشل مغلقًا. جرّب أيضًا مع معرف مستأجر غير صالح.
- تأكد من الأداء الأساسي: انظر خطة الاستعلام وتأكد من أن الفهارس تدعم نمط ترشيح المستأجر لديك (غالبًا
tenant_idزائد ما تقوم بالفرز أو البحث به).
إذا فاجأك أي اختبار، أصلح السياسة أو إعداد السياق أولًا. لا تطرّز الحل في الواجهة أو الـ API وتأمل أن قواعد القاعدة "ستصمد إلى حد كبير".
الخطوات التالية: النشر الآمن والحفاظ على الاتساق
عامل PostgreSQL RLS كنظام أمان: أدخله بحذر، تحقق منه كثيرًا، وحافظ على قواعد بسيطة بما يكفي ليتبعها فريقك.
ابدأ صغيرًا. اختر الجداول التي قد يضر تسريبها أكثر (المدفوعات، الفواتير، بيانات الموظفين، رسائل العملاء)، وفعل RLS هناك أولًا. الانتصارات المبكرة أفضل من نشر كبير لا يفهمه أحد تمامًا.
ترتيب نشر عملي غالبًا ما يكون:
- جداول "مملوكة" أساسية أولًا (الصفوف تنتمي بوضوح لمستأجر واحد)
- جداول تحتوي بيانات شخصية (PII)
- جداول مشتركة لكن مفلترة حسب المستأجر (التقارير، التحليلات)
- جداول الربط والحالات الحافة (علاقات متعددة إلى متعددة)
- الباقي بعد استقرار الأساس
اجعل الاختبار غير اختياري. يجب أن تشغل الاختبارات الآلية نفس الاستعلامات كأدوار ومستنئين مختلفة وتؤكد النتائج. أدرج اختبارات "يجب السماح" و"يجب الرفض"، لأن أخطر الأخطاء هي الإفراط في منح الأذونات بهدوء.
احتفظ بنقطة واحدة واضحة في تدفق الطلب حيث يُضبط سياق الجلسة قبل أي استعلام. يجب تطبيق tenant id وuser id وrole مرة واحدة، مبكرًا، وعدم التخمين لاحقًا. إذا ضبطت السياق في منتصف معاملة، سينتهي بك الأمر إلى تشغيل استعلام بقيم مفقودة أو قديمة.
عند البناء مع AppMaster، خطط لمزامنة بين واجهات الـ API المولدة وسياسات PostgreSQL. قم بتوحيد طريقة تمرير سياق المستأجر والدور إلى القاعدة (مثلاً نفس متغيرات الجلسة لكل نقطة نهاية) حتى تعمل السياسات بنفس الشكل في كل مكان. إذا كنت تستخدم AppMaster عند appmaster.io، يظل RLS هو السلطة النهائية لعزل المستأجرين، حتى لو قيدت الوصول في الواجهة أيضًا.
أخيرًا، راقب ما يفشل. فشلات التفويض إشارات مفيدة، خاصة بعد النشر. تتبع حالات الرفض المتكررة وابحث فيما إذا كانت تشير إلى هجوم حقيقي، مسار عميل معطّل، أو سياسة صارمة جدًا.
قائمة عادات قصيرة تساعد RLS على البقاء صحيًا:
- عقلية الرفض الافتراضي، مع إضافة الاستثناءات عن سبق إصرار
- أسماء سياسات واضحة (الجدول + الإجراء + الجمهور)
- مراجعة تغييرات السياسات مثل مراجعة الكود
- تسجيل ومراجعة حالات الرفض أثناء النشر المبكر
- إضافة مجموعة اختبارات صغيرة لكل جدول جديد مع RLS


