تعدّد المستأجرين والأمان على مستوى الصف
ما هو
ManpowerIQ متعدّدة المستأجرين: يخدم نشر واحد العديد من وحدات العمل (BUs)، وتُبقى بيانات التشغيل لكل وحدة عمل منفصلةً تمامًا. يُنفَّذ العزل في ثلاث طبقات مستقلة — مرشحات استعلام EF، ومعترض ختم الكتابة، وأمان PostgreSQL على مستوى الصف — بحيث لا تستطيع قطعة شيفرة واحدة تخطئ أن تُسرِّب بيانات عبر المستأجرين.
لماذا بُني بهذه الطريقة
فصل المستأجرين هو أصعب متطلّبات المنصة: يجب ألّا ترى الشركات أناس بعضها أو خططها أو جداولها أو تلمسها — لا عرَضًا، ولا عمدًا. حارس واحد (مجرّد مرشحات استعلام، مثلًا) سيكون على بُعد خطأ واحد من تسريب عبر المستأجرين. الدفاع المتعمّق بثلاث طبقات يعني أن الفجوة في إحداها تلتقطها التالية، وقاعدة البيانات هي خطّ الدفاع الأخير حتى لاستعلام خام.
قرار رئيسي: سلسلتا اتصال. يُستخدم اتصال المالك (manpoweriq، BYPASSRLS) للهجرات فقط؛ واتصال وقت التشغيل (manpoweriq_app) مقيَّد بـ RLS، فالتطبيق العامل لا يستطيع فيزيائيًا تجاوز الأمان على مستوى الصف (الورقة 01 §decisions، Program.cs:147-149، MIQ105_Report.md §7).
كيف يعمل
يحمل كل صفّ مملوك لمستأجر business_unit_id. تأتي هوية المستأجر من مطالبات JWT للمستخدم المُسجَّل دخوله وتتدفّق عبر الطبقات الثلاث جميعها:
flowchart TB
JWT[مطالبات JWT<br/>business_unit_id, is_super_admin, sub, permissions] --> CTP[CurrentTenantProvider<br/>يقرأ المطالبات عبر IHttpContextAccessor]
CTP --> L1[الطبقة 1 · مرشّحات استعلام EF<br/>WHERE business_unit_id = me]
CTP --> L2[الطبقة 2 · TenantStampingInterceptor<br/>ختم/حراسة business_unit_id عند الكتابة]
CTP --> MW[TenantContextMiddleware<br/>يضبط متغيّرات جلسة Postgres (GUCs)]
MW --> L3[الطبقة 3 · PostgreSQL RLS<br/>CREATE POLICY tenant_isolation]
L1 --> DB[(PostgreSQL)]
L2 --> DB
L3 --> DB
-
القراءات — مرشحات استعلام EF العامة. يطبّق
ManpowerIQDbContext.OnModelCreatingمرشّحHasQueryFilterلوحدة عمل + حذف ناعم على ~40 كيانًا (الورقة 01 §build-status،ManpowerIQDbContext.cs:117-321). القاعدة لكل كيان هي!IsDeleted && (BusinessUnitId == t.BusinessUnitId || t.IsSuperAdmin). هذه هي الطبقة العامة — كل كيان مستأجر يملكها. -
الكتابات —
TenantStampingInterceptor. عند إدراجTenantEntityبـBusinessUnitId == 0، يختم وحدة العمل الحالية؛ وإذا ضُبطت وحدة عمل مختلفة ولم يكن المستدعي super-admin فإنه يرمي "Cross-tenant insert blocked"؛ وعند التعديل، تغييرBusinessUnitIdيرمي "Tenant change is not allowed" (الورقة 01 §rules،TenantStampingInterceptor.cs:29-58). مُسجَّل على DbContext الإنتاجي (Program.cs:145,167-173). -
قاعدة البيانات — PostgreSQL RLS. 12 هجرة تشغّل
ENABLE/FORCE ROW LEVEL SECURITYخامًا +CREATE POLICY tenant_isolation(الورقة 01 §build-status، مثلًاMIQ105_AddUsersAndTenantRls.cs:95-108،MIQ003_AddRbac.cs:473-483). السياسة هيUSING (business_unit_id = current_setting('app.current_bu', true)::int OR current_setting('app.is_super_admin', true) = 'true'). وهيFORCEd فتلزم حتى مالكي الجداول.
حمل المستأجر إلى قاعدة البيانات. يضبط TenantConnectionInitializer متغيّرات جلسة GUC app.current_bu / app.is_super_admin لكل طلب (TenantConnectionInitializer.cs:26-41)، ويعمل TenantContextMiddleware بعد UseAuthentication وقبل UseAuthorization (الورقة 01 §build-status، Program.cs:476-478). يُقرأ المستأجر من JWT بواسطة CurrentTenantProvider الحقيقي (CurrentTenantProvider.cs:26-105).
البيانات العامة (غير المستأجرة). قلّة متعمَّدة من الجداول غير محصورة: Permission هو كتالوج عام بلا business_unit_id؛ وNodeType، وHolidayType، وShiftType، وقوائم مرجعية أخرى هي بيانات مرجعية مشتركة (الورقة 01 §rules، MUST-NOT #4). تسمح بعض الكيانات الهجينة (CertificationType، DemandReason، DemandTemplate، AllocationRuleTypeCatalog) بصفوف عامة (BusinessUnitId == null) وصفوف مملوكة لوحدة عمل معًا.
مزالق / قيود
- مزوّد المستأجر في وقت التشغيل هو
CurrentTenantProviderالحقيقي، لا نسخة وهمية. يوجدStubCurrentTenantProviderلكنه شيفرة ميتة غير مرجعية — لا تصف النظام بأنه "مُجزّأ وهميًا / BU=1 دائمًا". ملاحظة "stub" القديمة في MIQ-005 تاريخية (الورقة 01 MUST-NOT #1،Program.cs:179). - RLS حقيقي لكنه انتقائي؛ مرشحات الاستعلام عامة. يغطّي RLS قائمة سماح محدّدة من الجداول (users، terminals، departments، nodes، pools، audit_events، roles، role_permissions، user_roles، + جولات لاحقة)، وليس تلقائيًا كل
TenantEntity. مرشحات استعلام EF هي الطبقة التي تغطّي كل شيء (الورقة 01 MUST-NOT #2). - بديل التطوير. في
Developmentدون رمز، يُرجِعCurrentTenantProviderBU=1 / اسم المستخدم "dev" (الورقة 01 MUST-NOT #6،CurrentTenantProvider.cs:36,63). ملاءمة تطوير — لا مسار الإنتاج. - تجاوز super-admin متساهل بالـ GUC.
is_super_admin='true'يفتح RLS؛ ولا يجوز إلا للمسارات الموثوقة ضبط ذلك الـ GUC (الورقة 01 §decisions،MIQ105_Report.md §10.3). - تلتقط مرشحات الاستعلام
_tenantفي وقت بناء النموذج. الإنتاج آمن (هوية خيارات ثابتة = مدخل cache نموذج واحد)، لكن يجب أن تستخدم الاختباراتPerInstanceModelCacheKeyFactory(الورقة 01 edge-cases،Program.cs:150-173). - نطاق جلسة GUC يعتمد على إعادة Npgsql تعيين الاتصالات عند العودة إلى التجمّع لتجنّب التسرّب عبر الطلبات (الورقة 01 edge-cases،
TenantConnectionInitializer.cs:24-27).
حالة البناء
Available — تُسلَّم الطبقات الثلاث جميعها وتُنفَّذ: مرشحات الاستعلام (~40 كيانًا)، ومعترض الختم، وPostgres RLS (12 هجرة). مُتحقَّق منها LIVE (الورقة 01 §build-status).
ذات صلة
- المصادقة وRBAC — من أين تأتي مطالبات JWT التي تقود المستأجر.
- التدقيق والحذف الناعم — نصف الحذف الناعم من مرشّح الاستعلام.
- ورقة الحقائق: 01 (الأساس).