عزل المستأجرين على مستوى التطبيق يعتمد على تذكر كل مطور لكل شرط WHERE. أمان صفوف PostgreSQL يفرض العزل على مستوى قاعدة البيانات ذاتها. كيفية تطبيقه بشكل صحيح في Go وما الذي يكتشفه.
تعتمد معظم أنظمة Go SaaS على تصفية كل استعلام بشرط WHERE tenant_id = $1 لعزل بيانات المستأجرين. هذا النهج يعمل حتى يفشل. خطأ واحد في استعلام نادر الاستخدام، أو مهمة خلفية تتجاوز middleware المصادقة، أو مطور لا يعرف النمط، يُنتج تسرباً للبيانات بين مستأجرين مختلفين. أمان صفوف PostgreSQL (RLS) يفرض العزل على مستوى قاعدة البيانات ذاتها بشكل مستقل عما يرسله التطبيق.
لماذا عزل المستأجرين على مستوى التطبيق له نقطة فشل؟
في نظام SaaS متعدد المستأجرين بـ 50 جدولاً و200 دالة استعلام، كل مطور في كل طلب سحب يحتاج أن يتذكر إضافة شرط tenant_id لكل استعلام. غياب شرط واحد في استعلام تقارير نادر الاستخدام ينتج تسرباً للبيانات.
في لبنان والمنطقة العربية، أنظمة SaaS التي تتعامل مع بيانات مالية أو بيانات شخصية للعملاء أو بيانات تشغيلية لشركات منافسة تواجه عواقب قانونية وسمعة حقيقية إذا تسربت البيانات بين المستأجرين. الدفاع متعدد الطبقات يستحق جهد التنفيذ.
ما الذي يفعله RLS في PostgreSQL؟
أمان صفوف PostgreSQL هو ميزة تُلصق سياسات أمان بالجداول الفردية. بمجرد تفعيله، تُقيّم قاعدة البيانات شرط منطقي لكل وصول للصفوف، في كل استعلام، بصرف النظر عما يرسله التطبيق.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant_id')::bigint);
بعد تفعيل هذه السياسة، حتى الاستعلام الذي يجلب كل السجلات لا يُعيد سوى سجلات المستأجر الحالي.
إعداد سياسات RLS لمخطط متعدد المستأجرين
CREATE ROLE app_user;
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
AS PERMISSIVE FOR ALL TO app_user
USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::bigint);
شرط WITH CHECK يُطبق على عمليات INSERT و UPDATE. بدونه، خطأ في التطبيق قد يُدرج صفاً بـ tenant_id خاطئ دون أن يُكتشف.
نشر سياق المستأجر من Go عبر مجمع الاتصالات
التحدي في RLS أن قيمة current_setting مرتبطة بالجلسة. مع مجمع اتصالات مثل pgBouncer أو sql.DB في Go، تُعاد الاتصالات بين الطلبات. الطريقة الصحيحة هي تعيين السياق وإعادة تهيئته داخل معاملة:
func (r *OrderRepository) FindByID(ctx context.Context, tenantID int64, orderID int64) (*Order, error) {
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, "SELECT set_config('app.current_tenant_id', $1, true)",
strconv.FormatInt(tenantID, 10))
if err != nil {
return nil, err
}
var order Order
err = tx.QueryRowContext(ctx,
`SELECT id, tenant_id, total, status FROM orders WHERE id = $1`,
orderID,
).Scan(&order.ID, &order.TenantID, &order.Total, &order.Status)
if err != nil {
return nil, err
}
return &order, tx.Commit()
}
المعامل الثالث في set_config يتحكم في ما إذا كان الإعداد محلياً للمعاملة. تمرير true يعني إعادة تهيئة الإعداد تلقائياً عند انتهاء المعاملة.
اعتبارات الأداء
يُضيف RLS شرطاً لكل خطة استعلام. للبحث البسيط بالفهرس هذا لا يُذكر. في الجداول ذات الفهارس الجيدة، التأثير أقل من ميلي ثانية لكل استعلام. قسنا هذا على أنظمة Go SaaS في الإنتاج لعملاء في لبنان والمنطقة تُعالج 500 إلى 2000 طلب في الثانية على ECS Fargate، ولم يظهر RLS قط كعنق زجاجة في سجلات OpenTelemetry.
دروس من الإنتاج الفعلي في أنظمة MENA
عند طرح RLS على نظام SaaS لمطاعم في لبنان، اكتشفنا أن عدة مهام خلفية كانت تستخدم اتصالات بدون سياق مستأجر. هذه المهام كانت تُجمع بيانات عبر جميع المستأجرين بقصد، لكنها احتاجت دور قاعدة بيانات مخصص يتجاوز RLS بدلاً من استدعاء set_config مفقود.
اكتشفنا أيضاً أن اختبارات التكامل التي تُنشئ بيانات اختبار بدون سياق مستأجر فشلت الآن. الصفوف المُدرجة بدون سياق مطابق تنتهك سياسة WITH CHECK. هذا كشف عدة أخطاء في إعداد الاختبارات كانت تُدرج بيانات غير مقيدة بصمت.
الخلاصة
يُضيف RLS في PostgreSQL طبقة تطبيق على مستوى قاعدة البيانات تُعالج نوع الأخطاء في عزل المستأجرين التي يُفوتها كود التطبيق. التكلفة الرئيسية هي نشر سياق المستأجر بشكل صحيح عبر مجمع الاتصالات باستخدام إعدادات محلية للمعاملة.



