في العام الماضي، بعد ثلاثة أسابيع من العمل على مشروع لعميل في قطاع الإنشاءات في السعودية، لاحظت أن المستخدمين الداخليين يمكنهم رؤية بيانات من شركات منافسة. ما المشكلة؟ نقص في سياسات أمن الصفوف (Row-Level Security - RLS) في قاعدة بيانات PostgreSQL الخاصة بهم. لم يكن هذا مجرد خلل أكاديمي في التصميم، بل كان يتسبب بنشاط في تسريب بيانات حساسة بين المقاولين. كان علينا إصلاح المشكلة على الفور.
أمن الصفوف (RLS) في Postgres هو أحد تلك الأشياء التي يمكنك تجاهلها إلى أن تلدغك (وهو ما سيحدث بالتأكيد). عندما طلب هذا العميل تطبيق RLS، كنا قد كتبنا بالفعل 80٪ من الـ API باستخدام Prisma ORM. علمتني عملية إعادة تهيئة RLS في هذا الإعداد بعض الدروس القاسية حول الموازنة بين التحكم في قاعدة البيانات وراحة ORM.
كيف يعمل أمن الصفوف (Row-Level Security) فعليًا (بدون المصطلحات التقنية)
يعمل RLS في PostgreSQL من خلال السياسات. تحدد هذه السياسات أن "المستخدم X يمكنه فعل Y على الجدول Z فقط عندما يكون الشرط Q صحيحًا". على سبيل المثال: تطبيق رعاية صحية قمت ببنائه لعيادة في الشارقة لديه سياسة تنص على أنه يمكن للأطباء فقط تعديل سجلات المرضى الخاضعين لإشرافهم.
Prisma يعقّد هذا الأمر قليلًا لأنه يجرّد استعلامات قاعدة البيانات. إذا لم تكن حريصًا، فقد تتجاوز عمليات Prisma الخاصة بك سياسات Postgres. ما الحل؟ استخدام امتدادات عميل Prisma أو برنامج وسيط لتمرير سياق المستخدم إلى قاعدة البيانات. سأوضح لك كيف يتم ذلك بعد قليل.
إليك الحد الأدنى لتمكين RLS على جدول:
ALTER TABLE "Invoice" ENABLE ROW LEVEL SECURITY;
CREATE POLICY invoice_access ON "Invoice"
FOR ALL
TO authenticated
USING (CURRENT_USER = user_id::text);يمنع هذا الأشخاص غير المالكيين من لمس سجلات الفواتير - بافتراض أن user_id يطابق اسم دور Postgres الخاص بك.
تطبيق RLS عمليًا
استخدمت امتدادات جانب العميل في Prisma لحقن معرفات المستخدمين من مطالبات JWT:
- حدد السياسات في نماذج Prisma باستخدام التعليقات:
model Invoice {
id Int
user_id String
/// @policy("CURRENT_USER = user_id::text")
amount Decimal
}- إنشاء سياسات SQL من خلال عمليات الترحيل (migrations). أقوم بأتمتة هذا باستخدام برنامج نصي مخصص يحلل هذه التعليقات وينشئ سياسات Postgres.
- تمرير سياق المستخدم عبر برنامج وسيط Prisma. يقوم هذا البرنامج الوسيط بتعيين المستخدم الحالي قبل الاستعلامات:
prisma.$use(async (params, next) => {
if (params.model === 'Invoice') {
params.args.where = {
...params.args.where,
user_id: currentUser.id
};
}
return next(params);
});التفاصيل القبيحة التي تلدغك لاحقًا
يعمل CURRENT_USER في PostgreSQL بشكل رائع - إلى أن يقوم المستخدمون بتبديل الأدوار. كان لدى أحد عملائي في دبي مسؤولون يتمتعون بامتيازات تصعيد مؤقتة، لكن سياسات RLS الخاصة بهم اعتمدت بشكل صارم على CURRENT_USER. ما النتيجة؟ لم يتمكن المسؤولون من الإشراف على التقارير دون استخدام دور مستخدم متميز منفصل.
إليك ما تعلمته:
- •لا تقم بترميز
CURRENT_USERبشكل ثابت لتجاوزات المسؤول - •اختبر السياسات بحالات حافة مثل المستخدمين المعلقين أو الأشباح
- •استخدم مطالبات JSON في رموز JWT للأدوار المعقدة، وليس فقط أدوار قاعدة البيانات
ذات مرة أمضيت 4 ساعات في تصحيح الأخطاء لمعرفة سبب عدم تمكن مكالمة API في Laravel من العثور على سجلات - اتضح أن عميل Prisma لم يمرر تنسيق user_id المتوقع. رفضت سياسات PostgreSQL الاستعلامات بصمت.
مثال واقعي: منصة حجز سيارات الأجرة تواصُل ليمو
بالنسبة لـ Tawasul Limo، صممت سياسات RLS لفرض عزل السائق/العميل:
model Ride {
id Int
driver_id String
customer_id String
/// @policy("user_role = 'driver' AND driver_id = CURRENT_USER")
/// @policy("user_role = 'customer' AND customer_id = CURRENT_USER")
}يتيح هذا الإعداد للسائقين رؤية الرحلات المخصصة لهم فقط مع منع العملاء من تزوير الحجوزات بأسماء الآخرين. قمنا بتخزين user_role في مطالبة JWT ومررناها عبر برنامج وسيط Prisma.
واجهتنا عقبة عند تنفيذ إلغاء الرحلة - احتاج المسؤولون إلى رؤية عبر المنصة بأكملها. ما الحل؟ دور قاعدة بيانات منفصل لديه إذن لتجاوز RLS على جدول Ride باستخدام سلوك ALTER TABLE ... FORCE ROW LEVEL SECURITY الخاص بـ Postgres.
الأسئلة الشائعة
كيف تختبر سياسات أمن الصفوف محليًا قبل النشر؟
استخدم Docker لتكرار بيانات الإنتاج في مثيل Postgres المحلي لديك. أقوم بإنشاء مستخدمين اختبار بأدوار مختلفة، ثم أقوم بتشغيل الاستعلامات من خلال Prisma للتحقق من الوصول. تساعد أدوات مثل وضع عرض \x في psql في التحقق من تفاصيل السياسة صفًا تلو الآخر.
هل يمكن أن تتعارض سياسات RLS مع الحراس على مستوى التطبيق؟
قطعاً. كان لدى أحد العملاء حراس قائمين على Prisma وسياسات RLS ترفض نفس الاستعلام - وكان تصحيح هذا خطأً فادحًا. التزم بمصدر واحد للحقيقة: أوصي بإعطاء الأولوية لـ RLS لضمان اتساق البيانات، واستخدم الحراس على مستوى التطبيق فقط لأغراض تجربة المستخدم.
ما هي أفضل الممارسات لتدقيق سياسات RLS الحالية؟
قم بتشغيل SELECT * FROM pg_policy order by policyname; في Postgres لرؤية جميع السياسات النشطة. بالنسبة للأنظمة المعقدة، قم بتوثيق نية كل سياسة في ويكي مشترك. أقوم بتتبع تغييرات السياسة من خلال نصوص الترحيل بأسماء ملفات وصفية مثل 20240515_policy_fix_invoice_rls.sql.
كيف تتعامل مع RLS متعدد المستأجرين مع الموارد المشتركة؟
استخدم عمود "معرّف المستأجر" للموارد التي يمكن الوصول إليها عبر الفرق - مثل قوائم العقارات في الإمارات العربية المتحدة المشتركة بين وكلاء متعددين. ادمج هذا مع سياسات النطاق الشامل:
CREATE POLICY tenant_access ON "Property"
FOR SELECT
TO authenticated
USING (tenant_id = current_setting('app.tenant')::uuid);إذا كنت تقوم ببناء منصات SaaS أو أدوات داخلية حيث يكون عزل البيانات مهمًا، فإن RLS ليس اختياريًا. لقد قمت بتطبيقه عبر عشرات الشركات الناشئة والمؤسسات في الإمارات العربية المتحدة - بدءًا من منصة حجز سيارات ليموزين فاخرة في مدينة خليفة وصولًا إلى نظام إدارة مخزون الأدوية في الكويت. يمكنك قراءة المزيد عن منهجي لأمن المكدس الكامل في Laravel vs Node.js for UAE Web Projects.
هل تحتاج إلى مساعدة في تصميم مخططات Postgres آمنة وقابلة للتطوير؟ احجز استشارة مجانية لمناقشة مشروعك.