> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Authorization: كيف تجمع قرارات الصلاحيات في مكان واحد
- URL: https://arabroot.io/articles/الصلاحيات-المركزية-authorization-rbac-و-rebac/
- Published: 2026-10-05T15:15:00.000Z
- Updated: 2026-10-12T15:51:31.000Z
- Description: لماذا تتحول شروط الصلاحيات المتفرقة في الكود إلى ثغرات، وكيف تنقل القرار إلى محرك سياسات واحد.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك تطبيقاً للفواتير، فيه واجهة ويب وواجهة API لتطبيق الجوال وخدمة تصدر ملفات PDF ولوحة إدارة صغيرة، وقد ربطت الدخول بمزود هوية كما شرحنا في دليل [OAuth وOpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، فأصبح كل تطبيق يعرف بثقة من هو المستخدم الذي أرسل الطلب. ثم يأتي مدير المالية بسؤال يبدو بسيطاً: من يستطيع اليوم أن يرى الفاتورة رقم 42، ومن يستطيع أن يعتمدها، ولماذا؟ وعندما تبحث عن الإجابة تجد أنها ليست في مكان واحد، فهي موزعة على عشرات الشروط من نوع `if` في أربع خدمات كتبها أربعة مطورين في أوقات مختلفة، وكل خدمة تفهم القاعدة بطريقتها، وإحداها لا تتحقق من شيء أصلاً.

وهذا هو موضوع هذا المقال: التفويض Authorization، أي الإجابة عن سؤال «ماذا يحق لهذا المستخدم أن يفعل بهذا المورد الآن؟»، وكيف تنقله من شروط متفرقة في الكود إلى مكان واحد واضح يمكن قراءته واختباره ومراجعته. وقد عرفنا في دليل [إدارة الهوية والوصول IAM](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/#authorization) نموذجي RBAC وABAC وأقل صلاحية ممكنة بشكل مختصر، وهنا نتعمق في الجانب الذي يهم المطور، وسوف نناقش في هذا المقال ما يلي:

- الفرق بين المصادقة والتفويض، ولماذا تفشل الشروط المتفرقة في الكود، وكيف تتحول إلى ثغرات IDOR.
- نماذج الصلاحيات على مثال واحد: قوائم التحكم ACL والأدوار RBAC والخصائص ABAC والعلاقات ReBAC التي بدأت من ورقة Google Zanzibar، والسياسات ككود.
- من يطلب ومن يقرر ومن ينفذ: المصطلحات PEP وPDP وPAP وPIP بلغة بسيطة، والمكتبة مقابل الخدمة المركزية، وأين يكون الفحص.
- أين تقع ال Scopes والحقول داخل التوكن، ولماذا لا تضع كل الصلاحيات في JWT.
- المحركات مفتوحة المصدر وتراخيصها الحالية: OPA وCedar وOpenFGA وSpiceDB وOry Keto وCasbin وCerbos وTopaz وPermify.
- تجربة عملية: سياسة للفواتير في OPA مع اختباراتها، ونموذج علاقات في OpenFGA يتغير فيه الوصول بإضافة سطر واحد.
- الصلاحيات ككود في Kubernetes، ثم إرشادات التصميم والأخطاء الشائعة وقائمة تحقق.

## من أنت، وماذا يحق لك؟ المشكلة التي تبدأ بعد تسجيل الدخول

المصادقة Authentication تجيب عن سؤال «من أنت؟»، فهي التي تتحقق من كلمة المرور أو مفتاح Passkey أو تستقبل التوكن من مزود الهوية، وفي نهايتها يعرف التطبيق أن صاحب الطلب هو `bob`. أما التفويض Authorization فيجيب عن سؤال مختلف تماماً، وهو «هل يحق ل bob أن يعتمد الفاتورة 42؟»، والسبب في الفصل بينهما أن الأول يحدث مرة واحدة عند الدخول ويمكن أن يتولاه مزود هوية مركزي، أما الثاني فيحدث عند كل طلب، ويحتاج إلى معرفة المورد نفسه: لمن هذه الفاتورة، وكم مبلغها، وفي أي مؤسسة، وما حالتها. ومزود الهوية لا يعرف شيئاً عن فواتيرك، لذلك لا يستطيع أن يقرر عنك.

والطريقة الأولى التي يكتب بها أغلبنا التفويض هي الطريقة الطبيعية: شرط `if` داخل الدالة التي تعالج الطلب، يقارن دور المستخدم أو معرفه بحقل في السجل. وهذه الطريقة تعمل في البداية، ولكن مع نمو النظام تظهر مشاكلها كما تبين الصورة التالية:

![مخطط من لوحتين: على اليمين أربع خدمات هي web app وmobile API وPDF export وadmin panel، وفي كل منها شرط if مختلف للصلاحية، وخدمة التصدير بلا أي فحص وعليها حشرة، وتحتها سؤال من يستطيع رؤية الفاتورة 42 ولماذا دون إجابة. وعلى اليسار الخدمات الأربع نفسها تسأل نقطة قرار واحدة PDP تقرأ سياسة واحدة من Git وتكتب سجل قرارات، والإجابة واضحة: alice لأنها صاحبة الفاتورة وcarol لأنها محاسبة في المؤسسة acme](https://arabroot.io/content/images/2026/10/authorization-01-the-problem.webp)

الشروط المتفرقة تعطي أربع نسخ مختلفة من القاعدة، ونقطة القرار الواحدة تعطي إجابة واحدة يمكن تفسيرها

في الصورة أعلاه لاحظ التالي:

- **القاعدة نفسها مكتوبة أربع مرات وبأربعة أشكال:** واجهة الويب تسمح للمدير ولصاحب الفاتورة، وواجهة API تسمح لكل من في المؤسسة نفسها لأن المطور نسي شرط المالك، ولوحة الإدارة تثق بنطاق البريد الإلكتروني، وكل تعديل في القاعدة يحتاج إلى أن تتذكر الأماكن الأربعة، وغالباً سوف تنسى واحداً منها.
- **لا أحد يستطيع الإجابة عن «من يستطيع رؤية الفاتورة 42، ولماذا؟»:** لأن الإجابة ليست بيانات يمكن الاستعلام عنها، وإنما سلوك موزع في الكود، والطريقة الوحيدة لمعرفته هي قراءة كل الكود.
- **المراجعة والتدقيق Audit شبه مستحيلين:** فعندما يسأل المدقق «من اعتمد هذه الفاتورة، وعلى أي أساس سمح له النظام؟» لا يوجد سجل للقرار، وإنما سجل للطلب في أحسن الأحوال.
- **شرط واحد منسي يكفي لثغرة:** خدمة التصدير تأخذ رقم الفاتورة من الرابط وتعيد الملف دون أن تسأل لمن هو، وكل ما يحتاجه المخترق هو أن يغير الرقم في الرابط.

وهذه الثغرة الأخيرة لها اسم معروف هو الإشارة المباشرة غير الآمنة إلى الكائن Insecure Direct Object Reference واختصاراً IDOR، وقد شرحنا في دليل [المعرفات الفريدة](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%B1%D9%81%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D8%AF%D8%A9-uuid-%D9%88-ulid-%D9%88-snowflake/#security) لماذا لا يحميك المعرف العشوائي منها، فالمعرف ليس صلاحية، والحماية الوحيدة هي فحص التفويض في كل طلب. وهي جزء من فئة أوسع تضعها OWASP في المرتبة الأولى بين أخطر مخاطر تطبيقات الويب، وهي تعطل التحكم بالوصول Broken Access Control: ففي إصدار 2021 صعدت هذه الفئة من المرتبة الخامسة إلى الأولى، كما في [صفحة A01:2021](https://owasp.org/Top10/2021/A01%5F2021-Broken%5FAccess%5FControl/?ref=arabroot.io)، وفي إصدار 2025 حافظت على المرتبة الأولى، وتذكر [صفحة A01:2025](https://top10.owasp.org/2025/A01%5F2025-Broken%5FAccess%5FControl?ref=arabroot.io) أن كل التطبيقات التي اختبرت فيها وجد فيها شكل من أشكال هذا الخلل، وأن الفئة تضم 40 نوعاً من الأخطاء CWE، ومن أمثلتها المذكورة صراحة عرض حساب شخص آخر أو تعديله بمجرد تغيير معرفه، ومن أول توصياتها: ارفض افتراضياً Deny by Default كل ما ليس عاماً، وسجل حالات الرفض ونبه المديرين عند تكرارها.

### الحلول الأولى التي تخطر على البال، ولماذا لا تكفي

عندما يشعر الفريق بالمشكلة، فإن أول ما يفعله عادة هو أحد الحلول التالية، وكل واحد منها خطوة في الاتجاه الصحيح ولكنه يتوقف في منتصف الطريق:

1. **دالة مساعدة مشتركة:** يجمع الفريق الشروط في دالة واحدة مثل `can_approve(user, invoice)` داخل التطبيق، وهذا أفضل بكثير من الشروط المتفرقة، ولكنه يعمل فقط ما دام التطبيق واحداً ومكتوباً بلغة واحدة. فعندما تكتب خدمة التصدير بلغة Go وواجهة الويب بلغة Python، تعود القاعدة لتكتب مرتين، وتعود النسختان لتختلفا مع الوقت.
2. **مكتبة مشتركة بين الخدمات:** تنقل الدالة إلى مكتبة تستوردها كل خدمة، فتواجه مشكلة الإصدارات: الخدمة التي لم تحدث المكتبة منذ ثلاثة أشهر ما زالت تطبق القاعدة القديمة، ولا تعرف ذلك إلا عندما يحدث خطأ.
3. **وضع كل الصلاحيات في التوكن:** يضيف الفريق إلى JWT قائمة بكل ما يحق للمستخدم، فيكبر التوكن ويصبح قديماً بمجرد أن تتغير صلاحية، وسوف نناقش هذا الحل بالتفصيل في [قسم التوكنات](#tokens) لأنه شائع جداً.

والحل الأنسب هو أن تفصل القرار عن الكود: القاعدة تكتب مرة واحدة في مكان واحد، بلغة مخصصة لها، ويسأل كل تطبيق هذا المكان عند كل طلب، وهذا ما تفعله محركات الصلاحيات التي سوف نراها. ولكن قبل المحركات نحتاج إلى أن نتفق على طريقة التفكير في الصلاحيات نفسها، أي النموذج.

## نماذج الصلاحيات على مثال واحد: الفواتير

سوف نستخدم في بقية المقال مثالاً واحداً حتى تقارن النماذج على الأرض نفسها. لدينا مؤسستان `acme` و`globex` تستخدمان التطبيق نفسه، وفي `acme` موظفة اسمها `alice` أنشأت الفاتورة 42 بمبلغ 12000، ومحاسبان هما `bob` وحد الاعتماد عنده 5000 و`carol` وحدها 50000، وفي `globex` محاسب اسمه `dave`. والفواتير محفوظة في مجلدات، وكل مجلد يملكه فريق، مثل فريق المالية `finance`. والصورة التالية تبين كيف يجيب كل نموذج عن السؤال نفسه:

![مخطط من أربع لوحات على مثال الفاتورة 42: في ACL قائمة ملحقة بالفاتورة فيها alice read write وcarol read approve وbob read. وفي RBAC يوضع bob وcarol في دور accountant الذي يملك invoice:read وinvoice:approve. وفي ABAC تدخل خصائص المستخدم user.org وlimit وخصائص الفاتورة inv.org وamount إلى قاعدة approve if user.org == inv.org and inv.amount <= user.limit. وفي ReBAC مسار علاقات من alice عضو في team:finance الذي يملك المجلد invoices-2026 الذي يحتوي الفاتورة، مع نقاط القوة والضعف لكل نموذج](https://arabroot.io/content/images/2026/10/authorization-02-models.webp)

النموذج يحدد أين تحفظ الصلاحية: على الكائن، أو في الدور، أو في قاعدة على الخصائص، أو في العلاقات بين الكائنات

### قائمة التحكم بالوصول ACL: قائمة على كل كائن

قائمة التحكم بالوصول Access Control List واختصاراً ACL هي أقدم النماذج وأبسطها: كل كائن تلحق به قائمة تقول من يستطيع أن يفعل ماذا، فالفاتورة 42 لها قائمة فيها `alice: read, write` و`carol: read, approve`. وهذا ما تراه في صلاحيات الملفات في Linux وفي مشاركة ملف مع شخص بعينه. نقاط القوة: مباشرة وسهلة الفهم، والإجابة عن «من يستطيع رؤية هذا الكائن؟» موجودة في القائمة نفسها. نقاط الضعف: عندما يكون لديك عشرة آلاف فاتورة فلديك عشرة آلاف قائمة، وعندما ينضم محاسب جديد عليك أن تضيفه إلى كل قائمة، وعندما يغادر عليك أن تتذكر حذفه منها كلها.

### الصلاحيات حسب الدور RBAC، وانفجار الأدوار

في [دليل IAM](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/#authorization) عرفنا RBAC بأنها صلاحيات تعطى للأدوار Roles لا للأشخاص، ثم يوضع الشخص في الدور، ففي مثالنا يملك دور `accountant` الصلاحيتين `invoice:read` و`invoice:approve`، ويوضع فيه bob وcarol. وهذا يحل مشكلة ACL، فالمحاسب الجديد يضاف إلى دور واحد وينتهي الأمر. ولكن انظر ماذا يحدث عندما تأتي المتطلبات الحقيقية: المحاسب يعتمد فواتير مؤسسته فقط، وليس فواتير `globex`، وبحد أقصى للمبلغ يختلف من محاسب إلى آخر، ولا يعتمد فاتورة أنشأها بنفسه. الدور وحده لا يعرف المؤسسة ولا المبلغ ولا المالك، فيحاول الفريق أن يحل ذلك بأدوار جديدة مثل `accountant-acme-5k` و`accountant-acme-50k` و`accountant-globex-50k`، ومع عشر مؤسسات وخمسة حدود تصل إلى خمسين دوراً لصلاحية واحدة، وهذا هو انفجار الأدوار Role Explosion. نقاط القوة: سهلة القراءة والمراجعة، وتدعمها كل التطبيقات ومزودات الهوية. نقاط الضعف: لا تعبر عن الملكية ولا عن السياق ولا عن العلاقة بين المستخدم والمورد.

### الصلاحيات حسب الخصائص ABAC: قاعدة على الخصائص

في ABAC لا يحفظ القرار مسبقاً، وإنما يحسب عند كل طلب من خصائص Attributes المستخدم (مؤسسته، حد الاعتماد، قسمه) والمورد (مؤسسة الفاتورة، مبلغها، حالتها، تصنيفها) والسياق (الوقت، موقع الطلب، هل الجهاز مدار من المؤسسة)، كما يصفها دليل NIST [SP 800-162](https://csrc.nist.gov/pubs/sp/800/162/upd2/final?ref=arabroot.io). فقاعدة الاعتماد في مثالنا تكتب مرة واحدة: «يعتمد المحاسب الفاتورة إذا كانت من مؤسسته، وكانت مسودة، ومبلغها أقل من حده أو يساويه، ولم يكن هو من أنشأها»، ولا تحتاج إلى أي دور جديد عندما تنضم مؤسسة جديدة. وهذا ما سوف نكتبه حرفياً في التجربة العملية بلغة Rego. نقاط القوة: تعبر عن الحدود والسياق والملكية بقاعدة واحدة. نقاط الضعف: السؤال العكسي «ما الفواتير التي يستطيع bob رؤيتها؟» يحتاج إلى تقييم القاعدة على كل فاتورة، والخصائص يجب أن تكون حديثة، فإذا نقل الموظف من قسم إلى آخر ولم تحدث خصائصه، فالقرار مبني على معلومة قديمة.

### الصلاحيات حسب العلاقات ReBAC: من أين جاءت Zanzibar؟

فكر الآن في Google Drive: المستند داخل مجلد، والمجلد مشارك مع فريق، والفريق فيه أعضاء، فمن يستطيع قراءة المستند؟ كل عضو في الفريق، دون أن يكتب أحد اسمه على المستند. وهذا هو جوهر الصلاحيات حسب العلاقات Relationship-Based Access Control واختصاراً ReBAC: الصلاحية لا تحفظ كقاعدة ولا كقائمة، وإنما تستنتج من مسار علاقات بين الكائنات، ففي مثالنا `alice` عضو في `team:finance`، والفريق يملك المجلد `invoices-2026`، والمجلد يحتوي الفاتورة، إذن alice تستطيع قراءة الفاتورة.

وقد انتشر هذا النموذج بعد أن نشرت Google ورقة [Zanzibar: Google's Consistent, Global Authorization System](https://www.usenix.org/conference/atc19/presentation/pang?ref=arabroot.io) في مؤتمر USENIX ATC عام 2019، وفيها تصف النظام الذي يقرر الصلاحيات في خدمات مثل Calendar وCloud وDrive وMaps وPhotos وYouTube، ويذكر ملخص الورقة أنه يدير تريليونات من قوائم التحكم ويعالج ملايين طلبات التفويض في الثانية، بزمن استجابة أقل من 10 مللي ثانية عند النسبة المئوية 95، وتوفر أعلى من 99.999% خلال ثلاث سنوات من التشغيل. والفكرة الأساسية في الورقة هي أن كل الصلاحيات تحفظ كعلاقات صغيرة بشكل موحد `object#relation@user`، مثل `doc:readme#viewer@group:eng#member` ومعناها «أعضاء المجموعة eng يستطيعون قراءة المستند readme»، ثم يصف ملف إعداد كيف تشتق العلاقات من بعضها، مثل «من يقرأ المجلد يقرأ ما بداخله». وعلى هذه الفكرة بنيت أغلب المحركات الحديثة مثل OpenFGA وSpiceDB وOry Keto وPermify.

نقاط القوة: تعبر بشكل طبيعي عن المشاركة والمجلدات والفرق والمؤسسات المتداخلة، وتجيب عن السؤال العكسي «ما الذي يستطيع alice رؤيته؟» مباشرة، وتغيير الوصول هو إضافة علاقة أو حذفها دون تغيير أي كود. نقاط الضعف: العلاقات بيانات يجب أن تبقى متزامنة مع بيانات تطبيقك، فإذا نقلت الفاتورة إلى مجلد آخر في قاعدة بياناتك ونسيت تحديث العلاقة في المحرك، فالقرار مبني على مكان قديم.

### السياسات ككود Policy as Code

بقي مصطلح سوف تسمعه كثيراً: الصلاحيات المبنية على السياسات Policy-Based Access Control واختصاراً PBAC، أو السياسات ككود Policy as Code. وهذا ليس نموذجاً خامساً بقدر ما هو طريقة في كتابة النماذج السابقة: القواعد تكتب في ملفات نصية بلغة مخصصة مثل Rego أو Cedar، وتحفظ في Git، وتختبر وتراجع في طلب دمج Pull Request، ثم تنشر إلى المحرك مثل أي كود آخر. والفائدة أن الإجابة عن «لماذا سمح النظام بهذا؟» أصبحت ملفاً تقرؤه، وتاريخ تغييره في Git يخبرك من غير القاعدة ومتى.

### أي نموذج تختار؟

النماذج لا تلغي بعضها، وأغلب الأنظمة الحقيقية تخلطها: تبدأ بأدوار قليلة واضحة مثل `admin` و`accountant` و`employee`، ثم تضيف شروط الخصائص حيث تظهر الحدود والسياق، مثل حد المبلغ والمؤسسة، ثم تضيف العلاقات عندما تظهر المشاركة والتسلسل الهرمي، مثل «شارك هذا المجلد مع فريق المراجعة». لذلك لا تبدأ بمحرك علاقات لتطبيق فيه ثلاثة أدوار، والسبب أن كل نموذج أعقد من سابقه يضيف بيانات يجب أن تحافظ عليها، وابدأ بأبسط نموذج يعبر عن متطلباتك الحالية، وانتقل إلى التالي عندما تبدأ الأدوار بالتكاثر.

## من يطلب، ومن يقرر، ومن ينفذ؟

عندما تفصل القرار عن الكود تظهر أدوار جديدة في النظام، ولها أسماء جاءت من معيار [XACML 3.0](https://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html?ref=arabroot.io) الذي اعتمدته OASIS في يناير 2013\. واللغة نفسها لم تعد شائعة في التطبيقات الحديثة، ولكن مصطلحاتها بقيت وتجدها في توثيق كل المحركات، لذلك يحسن أن تعرفها:

- **نقطة تنفيذ السياسة Policy Enforcement Point واختصاراً PEP:** الجزء من تطبيقك الذي يستقبل الطلب ويسأل عن القرار ثم ينفذه، أي يكمل الطلب أو يعيد 403\. ويعرفها المعيار بأنها الجهة التي تؤدي التحكم بالوصول بإرسال طلبات القرار وتنفيذ قرارات التفويض.
- **نقطة قرار السياسة Policy Decision Point واختصاراً PDP:** المحرك الذي يقيم السياسة ويعطي القرار، مثل OPA أو OpenFGA.
- **نقطة إدارة السياسة Policy Administration Point واختصاراً PAP:** المكان الذي تكتب فيه السياسات وتدار، وهو في الغالب اليوم مستودع Git مع مراجعة، أو واجهة إدارة.
- **نقطة معلومات السياسة Policy Information Point واختصاراً PIP:** مصدر الحقائق التي يحتاجها القرار، مثل قاعدة بيانات الفواتير ومجموعات المستخدم في مزود الهوية وبيانات الموارد البشرية.

والصورة التالية تبين هذه الأدوار في طلب واحد، عندما يحاول bob اعتماد الفاتورة 42:

![مخطط مرقم: 1 يرسل bob الطلب POST /invoices/42/approve إلى تطبيق الفواتير وهو PEP، 2 يسأل التطبيق محرك القرار PDP بالمدخل user bob وaction approve وresource invoice:42، 3 يحصل المحرك من PIP مثل قاعدة البيانات ومزود الهوية على bob.limit=5000 وinv.amount=12000، والسياسة محملة مسبقاً من PAP أي مستودع Git، 4 يعيد المحرك deny مع السبب amount above limit، 5 يعيد التطبيق 403 Forbidden إلى bob، 6 يكتب المحرك القرار في سجل القرارات](https://arabroot.io/content/images/2026/10/authorization-03-pep-pdp.webp)

التطبيق لا يقرر وإنما يسأل وينفذ، والمحرك لا ينفذ وإنما يقرر ويسجل لماذا قرر

في الصورة أعلاه لاحظ أن السياسة لا تقرأ من Git عند كل طلب، وإنما تحمل إلى المحرك مسبقاً، ففي OPA مثلاً تنشر السياسات والبيانات كحزمة [Bundle](https://www.openpolicyagent.org/docs/management-bundles?ref=arabroot.io) يسحبها المحرك دورياً ويحفظها في الذاكرة، لذلك يكون القرار سريعاً. ولاحظ أيضاً أن المدخل الذي يرسله التطبيق صغير وثابت الشكل: من هو المستخدم، وما الفعل، وما المورد، وهذا الشكل نفسه هو ما وحدته مجموعة AuthZEN في مؤسسة OpenID في معيار [Authorization API 1.0](https://openid.net/specs/authorization-api-1%5F0.html?ref=arabroot.io) الذي أصبح نهائياً في 11 يناير 2026، والهدف منه أن يتحدث أي PEP مع أي PDP بالواجهة نفسها، فتستطيع أن تغير المحرك دون أن تعيد كتابة التطبيقات.

### مكتبة داخل التطبيق أم خدمة مركزية؟

وقد يتساءل البعض: إذا كان كل طلب يحتاج إلى سؤال المحرك، ألا يضيف ذلك زمناً لكل طلب؟ والإجابة نعم، ولذلك توجد ثلاث طرق لتشغيل نقطة القرار، ولكل منها ثمن:

- **مكتبة داخل التطبيق Embedded Library:** المحرك يعمل داخل عملية التطبيق نفسها، فلا يوجد اتصال شبكة والقرار يستغرق أجزاء من المللي ثانية. وهذا ما تفعله Casbin، وما يمكن أن تفعله مع OPA عبر واجهة Go أو بتحويل السياسة إلى WebAssembly كما في [توثيق التكامل في OPA](https://www.openpolicyagent.org/docs/integration?ref=arabroot.io)، ومع Cedar التي تأتي كمكتبة. وثمنها أن كل تطبيق يحتاج إلى نسخة محدثة من السياسة، وأن اللغة يجب أن تكون مدعومة.
- **خدمة بجانب التطبيق Sidecar:** نسخة من المحرك تعمل بجانب كل تطبيق على السيرفر نفسه أو في ال Pod نفسه، ويسألها التطبيق عبر localhost، وهي الطريقة الأكثر شيوعاً مع OPA كما يذكر توثيقها. والاتصال محلي وسريع، والسياسة تصلها من مكان مركزي كحزمة.
- **خدمة مركزية Central Service:** محرك واحد أو عنقود يسأله الجميع عبر الشبكة، وهذا هو الشكل الطبيعي لمحركات العلاقات مثل OpenFGA وSpiceDB، لأن العلاقات بيانات كبيرة تحفظ في قاعدة بيانات ولا يمكن نسخها إلى كل تطبيق. وثمنها اتصال عبر الشبكة لكل قرار، والحاجة إلى أن تكون الخدمة متاحة دائماً، فإذا توقفت توقف كل شيء، وهذا هو السلوك الصحيح لأن البديل هو أن يسمح التطبيق بكل شيء عندما لا يجد من يسأله.

والتخزين المؤقت Caching يقلل الزمن، ولكنه يأتي بمشكلة القرار القديم: إذا خزنت قرار «bob يستطيع رؤية الفاتورة» لخمس دقائق ثم سحبت منه الصلاحية، فهو يستطيع رؤيتها خمس دقائق أخرى. لذلك اجعل مدة التخزين قصيرة، واعرف ما يفعله المحرك نفسه، وسوف نعود إلى هذه النقطة في [قسم الاتساق](#consistency).

### أين يكون الفحص: البوابة أم التطبيق؟

في دليل [حماية أي تطبيق باستخدام authentik Forward Auth](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/) وضعنا أمام التطبيق بوابة تسأل authentik قبل أن يصل أي طلب، فهل يغني ذلك عن التفويض داخل التطبيق؟ والإجابة لا، والسبب كما تبين الصورة التالية أن البوابة لا ترى الفاتورة:

![مخطط من ثلاث مناطق مع خطوات مرقمة: 1 يصل طلب bob إلى البوابة مع Forward Auth وهي تفحص فحصاً عاماً: هل سجل الدخول، هل هو في مجموعة finance-app، هل المسار admin للمديرين فقط، ولا تستطيع أن ترى الفاتورة 42. 2 يمر الطلب إلى تطبيق الفواتير PEP الذي يقرأ الفاتورة من قاعدة البيانات ويسأل هل يستطيع bob اعتماد invoice:42 ويرشح القوائم. 3 يسأل التطبيق المحرك المركزي PDP الذي يحمل السياسات والعلاقات، 4 فيعيد deny. وفوق ذلك توكن فيه sub وgroups وscope ويعرف من أنت وصلاحيات عامة لا لكل فاتورة](https://arabroot.io/content/images/2026/10/authorization-05-layers.webp)

البوابة تفحص فحصاً عاماً، والتوكن يعرف من أنت ومجموعاتك، والقرار الدقيق لكل فاتورة يبقى داخل التطبيق

في الصورة أعلاه لاحظ التالي:

- **الفحص العام Coarse-Grained عند البوابة:** هل سجل المستخدم الدخول؟ هل هو في المجموعة المسموح لها بفتح هذا التطبيق أصلاً؟ هل المسار `/admin` للمديرين فقط؟ وهذا مفيد جداً، لأنه يمنع الغرباء من الوصول إلى التطبيق قبل أن تصل طلباتهم إلى الكود، ولكنه لا يعرف شيئاً عن البيانات.
- **الفحص الدقيق Fine-Grained داخل التطبيق:** التطبيق وحده يقرأ الفاتورة 42 من قاعدة البيانات ويعرف لمن هي وكم مبلغها، لذلك هو من يجب أن يسأل المحرك «هل يستطيع bob اعتماد هذه الفاتورة؟» عند كل طلب، وعلى السيرفر دائماً.
- **القوائم تحتاج إلى معاملة خاصة:** صفحة «فواتيري» لا تسأل عن فاتورة واحدة، وإنما عن كل ما يستطيع المستخدم رؤيته، فإذا جلبت ألف فاتورة ثم سألت المحرك عن كل واحدة فالصفحة بطيئة. ومحركات العلاقات تجيب عن هذا السؤال مباشرة بأوامر مثل `list-objects` كما سوف نرى في التجربة، وأما مع محركات السياسات فالحل الشائع أن تضع شرط المؤسسة في استعلام قاعدة البيانات نفسه، ثم تسأل المحرك عن العمليات الحساسة.

## أين تقع ال Scopes والحقول داخل التوكن؟

هناك خلط شائع بين ثلاثة أشياء تبدو متشابهة، وكلها تظهر في التوكن الذي يصل إلى تطبيقك:

- **النطاقات Scopes في OAuth:** كما شرحنا في دليل [OAuth وOpenID Connect](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#scopes)، ال Scope يحدد ما وافق المستخدم على أن يفعله تطبيق آخر نيابة عنه، مثل `invoices:read`، فهو تفويض للتطبيق العميل Delegated Access وليس صلاحية المستخدم نفسه على فاتورة بعينها. فإذا أعطيت تطبيق التقارير Scope للقراءة، فهذا يعني أن التطبيق يستطيع أن يقرأ ما يستطيع المستخدم قراءته، ولا يعني أن المستخدم يستطيع قراءة كل الفواتير. لذلك ال Scope فحص عام يضاف إلى فحص التفويض ولا يحل محله.
- **المجموعات والأدوار من مزود الهوية:** مزود الهوية يستطيع أن يضع في التوكن حقلاً مثل `groups: ["finance"]`، وهو مصدر ممتاز للأدوار العامة لأن المجموعات تدار مركزياً، وقد شرحنا في دليل [SCIM](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-scim-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%B3%D8%A7%D8%A8%D8%A7%D8%AA-%D8%AA%D9%84%D9%82%D8%A7%D8%A6%D9%8A%D8%A7/#lab-groups) كيف تصل المجموعات إلى التطبيقات تلقائياً، وعند المغادرة تسحب. وهذه المجموعات تصبح أحد مدخلات القرار، أي خصائص يقرأها المحرك.
- **الصلاحية الدقيقة على مورد بعينه:** «bob يعتمد الفاتورة 42» لا مكان لها في التوكن، لأنها تعتمد على الفاتورة وعلى حالتها الآن.

وقد يتساءل البعض: لماذا لا نضع كل صلاحيات المستخدم في JWT، فلا نحتاج إلى سؤال أي محرك؟ والإجابة أن لهذا الحل مشكلتين لا يمكن تجاوزهما. الأولى هي الحجم، فالتوكن يرسل مع كل طلب، وغالباً داخل ملف تعريف ارتباط له حد للحجم كما في [دليل الجلسات وJWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#cookie-limits)، وقائمة بآلاف الفواتير لا تدخل فيه. والثانية هي القدم Staleness، فالتوكن يصدر مرة ويبقى صالحاً حتى ينتهي، وكما شرحنا في [مشكلة إلغاء JWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#jwt-revocation)، إذا سحبت من bob صلاحية بعد إصدار التوكن فالتوكن ما زال يقول إنه يملكها. لذلك اجعل التوكن يحمل الهوية والمجموعات العامة فقط، ودع المحرك يقرر الصلاحية الدقيقة عند كل طلب.

## المحركات مفتوحة المصدر: ماذا تختار؟

فيما يلي المحركات التي سوف تقابلها، وقد راجعنا الترخيص والحالة لكل منها من مستودعها الرسمي في أكتوبر 2026، لأن بعضها تغير مالكه أو حالته خلال السنتين الماضيتين.

### Open Policy Agent واختصاراً OPA

[OPA](https://github.com/open-policy-agent/opa?ref=arabroot.io) محرك سياسات عام مكتوب بلغة Go بترخيص Apache-2.0، والسياسات فيه تكتب بلغة [Rego](https://www.openpolicyagent.org/docs/policy-language?ref=arabroot.io)، وهو لا يعرف شيئاً عن الفواتير أو Kubernetes، وإنما يأخذ مدخلاً بصيغة JSON وبيانات وسياسة ويعيد قراراً بصيغة JSON، لذلك يستخدم في التطبيقات وفي Kubernetes وفي فحص ملفات Terraform وغيرها. وقد قبل في CNCF عام 2018، ووصل إلى مستوى Graduated في 29 يناير 2021 كما في [صفحته في CNCF](https://www.cncf.io/projects/open-policy-agent-opa/?ref=arabroot.io). نقاط القوة: عام لكل شيء، وناضج جداً، وفيه اختبارات للسياسات وسجل للقرارات وتوزيع للسياسات كحزم، ويعمل كخدمة أو Sidecar أو مكتبة Go أو WebAssembly. نقاط الضعف: لغة Rego تحتاج إلى وقت للتعود عليها، وهو ليس مصمماً لحفظ ملايين العلاقات، فالبيانات التي يحتاجها يجب أن تصله مع المدخل أو كحزمة.

### Cedar

[Cedar](https://github.com/cedar-policy/cedar?ref=arabroot.io) لغة سياسات ومحرك مكتوب بلغة Rust، نشرته AWS كمصدر مفتوح بترخيص Apache-2.0 في [مايو 2023](https://aws.amazon.com/about-aws/whats-new/2023/05/cedar-open-source-language-access-control/?ref=arabroot.io)، وهو المحرك الذي تقوم عليه خدمة Amazon Verified Permissions، وقد قبل في CNCF بمستوى Sandbox في 8 أكتوبر 2025 كما في [صفحته في CNCF](https://www.cncf.io/projects/cedar/?ref=arabroot.io). والسياسات فيه تكتب بصيغة `permit` و`forbid` قريبة من اللغة الطبيعية، وقاعدة `forbid` تغلب `permit` دائماً، واللغة مصممة لتكون قابلة للتحليل الآلي، فتستطيع أن تتحقق بأدوات رياضية من أن تعديلاً ما لا يوسع الصلاحيات. نقاط القوة: لغة سهلة القراءة، وتعبر عن RBAC وABAC وجزء من العلاقات، وسريعة لأنها مكتبة. نقاط الضعف: تأتي كمكتبة بالدرجة الأولى، فعليك أن تبني حولها توزيع السياسات والسجلات بنفسك، والمكتبة الأساسية بلغة Rust مع مكتبة رسمية [cedar-go](https://github.com/cedar-policy/cedar-go?ref=arabroot.io) للغة Go.

### OpenFGA

[OpenFGA](https://github.com/openfga/openfga?ref=arabroot.io) محرك علاقات مستوحى من Zanzibar، بدأه موظفون في Okta وهو أساس الخدمة التجارية Auth0 FGA، ونشر كمصدر مفتوح في يونيو 2022، ويكتب بلغة Go بترخيص Apache-2.0، وقد انتقل في CNCF إلى مستوى Incubating في 28 أكتوبر 2025 كما في [صفحته في CNCF](https://www.cncf.io/projects/openfga/?ref=arabroot.io) و[إعلان المشروع](https://openfga.dev/blog/incubation-announcement?ref=arabroot.io). والنموذج فيه يكتب بلغة DSL بسيطة، والعلاقات تحفظ في PostgreSQL أو MySQL أو SQLite، ويدعم الشروط [Conditions](https://openfga.dev/docs/modeling/conditions?ref=arabroot.io) لإضافة خصائص إلى العلاقة، مثل علاقة صالحة حتى تاريخ معين. نقاط القوة: نموذج سهل القراءة، وأوامر جاهزة للسؤال العكسي مثل `list-objects` و`list-users`، وأداة سطر أوامر تختبر النموذج. نقاط الضعف: عليك أن تبقي العلاقات متزامنة مع بيانات تطبيقك، وهو خدمة مركزية يجب أن تكون متاحة دائماً.

### SpiceDB

[SpiceDB](https://github.com/authzed/spicedb?ref=arabroot.io) من شركة AuthZed، بترخيص Apache-2.0 ومكتوب بلغة Go، ويصف نفسه في [توثيقه](https://authzed.com/docs/spicedb/getting-started/discovering-spicedb?ref=arabroot.io) بأنه محرك قريب جداً من البنية التي وصفتها ورقة Zanzibar، ويحفظ العلاقات في PostgreSQL أو MySQL أو CockroachDB أو Spanner أو في الذاكرة للتجارب. وأهم ما يميزه أنه يطبق حل Zanzibar لمشكلة «العدو الجديد» عبر رموز الاتساق ZedTokens، وسوف نشرح هذه المشكلة في [قسم الاتساق](#consistency). نقاط القوة: الأقرب إلى Zanzibar في الاتساق والأداء على الأحجام الكبيرة، ولغة Schema واضحة مع أدوات تحقق. نقاط الضعف: يحتاج إلى فهم جيد لنموذج الاتساق، وبعض الميزات المتقدمة في الخدمة التجارية.

### Ory Keto

[Ory Keto](https://github.com/ory/keto?ref=arabroot.io) من شركة Ory بترخيص Apache-2.0 ومكتوب بلغة Go، ويصف نفسه في [توثيقه](https://www.ory.sh/docs/keto?ref=arabroot.io) بأنه أول تطبيق مفتوح المصدر لمبادئ Zanzibar، والنموذج فيه يكتب بلغة Ory Permission Language وهي جزء من TypeScript. نقاط القوة: يتكامل مع بقية أدوات Ory مثل Kratos وHydra إذا كنت تستخدمها. نقاط الضعف: مجتمعه وأدواته أصغر من OpenFGA وSpiceDB، ونشاط المستودع أقل منهما.

### Casbin

[Casbin](https://github.com/apache/casbin?ref=arabroot.io) مكتبة وليست خدمة، بترخيص Apache-2.0، ولها نسخ رسمية بلغات كثيرة منها Go وJava وNode.js وPHP وPython وC# وRust كما في [توثيقها](https://casbin.org/docs/overview?ref=arabroot.io)، والنموذج فيها يوصف في ملف إعداد بطريقة PERM (السياسة Policy والأثر Effect والأدوار Role والمطابقة Matchers)، فتستطيع أن تعبر به عن ACL وRBAC وABAC. وقد دخل المشروع حاضنة مؤسسة Apache في 7 فبراير 2026 كما في [صفحة الحاضنة](https://incubator.apache.org/clutch/casbin.html?ref=arabroot.io)، لذلك انتقل المستودع إلى `apache/casbin`. نقاط القوة: لا يحتاج إلى أي خدمة إضافية، ويعمل داخل تطبيقك بلغتك. نقاط الضعف: كل تطبيق يحمل نسخته من القواعد، فإذا كان لديك عدة تطبيقات بلغات مختلفة عدت إلى مشكلة النسخ المتعددة، إلا إذا حفظت السياسات في قاعدة بيانات مشتركة عبر المحولات Adapters.

### Cerbos

[Cerbos](https://github.com/cerbos/cerbos?ref=arabroot.io) نقطة قرار بترخيص Apache-2.0 ومكتوبة بلغة Go، والسياسات فيها ملفات YAML مع شروط بلغة CEL، وهي بلا حالة Stateless كما في [توثيقها](https://docs.cerbos.dev/cerbos/latest/?ref=arabroot.io)، أي أنها لا تحفظ بيانات المستخدمين ولا الموارد، وإنما يرسلها التطبيق مع كل طلب، وتعمل كخدمة أو Sidecar، وتدعم واجهة AuthZEN. وللشركة خدمة تجارية اسمها Cerbos Hub لإدارة السياسات وتوزيعها وجمع السجلات. نقاط القوة: السياسات بصيغة YAML يقرؤها من لا يعرف لغة برمجة، وفيها أدوات اختبار. نقاط الضعف: لا تحفظ العلاقات، فالمشاركة والمجلدات المتداخلة تحتاج إلى أن يجهز التطبيق بياناتها قبل السؤال.

### Topaz

[Topaz](https://github.com/aserto-dev/topaz?ref=arabroot.io) بترخيص Apache-2.0، ويجمع بين OPA كمحرك للسياسات ودليل مدمج مستوحى من Zanzibar للعلاقات، فتكتب السياسة بلغة Rego وتسأل فيها عن العلاقات. وقد أعلنت الشركة التي طورته Aserto في [إعلانها الأخير](https://www.aserto.com/blog/the-final-chapter-for-aserto?ref=arabroot.io) إغلاق خدمتها السحابية بحلول 31 مايو 2025، مع استمرار المشروع المفتوح عبر مشرفيه. نقاط القوة: يجمع السياسات والعلاقات في محرك واحد. نقاط الضعف: لم تعد خلفه شركة، لذلك راجع نشاط المستودع ومن يدعمه قبل أن تبني عليه نظاماً جديداً.

### Permify

[Permify](https://github.com/Permify/permify?ref=arabroot.io) محرك علاقات مستوحى من Zanzibar ومكتوب بلغة Go، ويدعم RBAC وReBAC وABAC بلغة نمذجة خاصة به، وترخيصه AGPL-3.0، وهذا يختلف عن بقية القائمة، فإذا عدلت الكود وقدمته كخدمة للآخرين فعليك أن تنشر تعديلاتك، وقد شرحنا معنى ذلك في [دليل التراخيص](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/). وقد استحوذت عليه FusionAuth في [نوفمبر 2025](https://fusionauth.io/blog/fusionauth-acquires-permify?ref=arabroot.io) مع التزام معلن بالاستمرار في دعم المصدر المفتوح. نقاط القوة: يجمع العلاقات والخصائص في نموذج واحد. نقاط الضعف: الترخيص يحتاج إلى مراجعة قانونية في بعض المؤسسات، ومستقبله مرتبط بخطة الشركة المالكة.

### مقارنة سريعة

| المحرك   | النموذج                   | اللغة            | طريقة التشغيل                 | الترخيص    | يناسب                                             |
| -------- | ------------------------- | ---------------- | ----------------------------- | ---------- | ------------------------------------------------- |
| OPA      | سياسات (RBAC وABAC)       | Rego             | خدمة، Sidecar، مكتبة Go، Wasm | Apache-2.0 | سياسات عامة للتطبيقات والبنية التحتية وKubernetes |
| Cedar    | سياسات مع شيء من العلاقات | Cedar            | مكتبة Rust وGo                | Apache-2.0 | قواعد مقروءة قابلة للتحليل داخل التطبيق           |
| OpenFGA  | علاقات ReBAC مع شروط      | OpenFGA DSL      | خدمة                          | Apache-2.0 | المشاركة والمجلدات والفرق والمؤسسات               |
| SpiceDB  | علاقات ReBAC              | SpiceDB Schema   | خدمة                          | Apache-2.0 | علاقات كبيرة الحجم مع اتساق صارم                  |
| Ory Keto | علاقات ReBAC              | OPL (TypeScript) | خدمة                          | Apache-2.0 | من يستخدم بقية أدوات Ory                          |
| Casbin   | ACL وRBAC وABAC           | نموذج PERM       | مكتبة بلغات كثيرة             | Apache-2.0 | تطبيق واحد يريد صلاحيات دون خدمة إضافية           |
| Cerbos   | سياسات (RBAC وABAC)       | YAML وCEL        | خدمة أو Sidecar               | Apache-2.0 | سياسات يقرؤها غير المبرمجين                       |
| Topaz    | سياسات وعلاقات            | Rego مع دليل     | خدمة أو Sidecar               | Apache-2.0 | من يريد الاثنين معاً ويقبل غياب الشركة            |
| Permify  | علاقات وخصائص             | Permify Schema   | خدمة                          | AGPL-3.0   | العلاقات والخصائص في نموذج واحد                   |

والقاعدة العملية في الاختيار: إذا كانت متطلباتك قواعد على خصائص، مثل الحدود والمؤسسة والوقت، فابدأ بمحرك سياسات مثل OPA أو Cedar أو Cerbos. وإذا كانت مشاركة وتسلسلاً هرمياً، مثل مجلدات ومستندات وفرق، فابدأ بمحرك علاقات مثل OpenFGA أو SpiceDB. وإذا كان لديك تطبيق واحد صغير بلغة واحدة، فمكتبة مثل Casbin تكفيه دون خدمة جديدة تديرها.

وإذا كانت لديك عدة خدمات تحتاج إلى نقطة القرار نفسها، فقد شرحنا كيف تشغلها كمنصة مشتركة، وكيف تبقي علاقاتها متزامنة مع بيانات كل خدمة، في [منصة صلاحيات مركزية لخدماتك](https://arabroot.io/articles/%D9%85%D9%86%D8%B5%D8%A9-%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-%D9%84%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%D9%83-microservices/).

## تجربة عملية: سياسة في OPA وعلاقات في OpenFGA

في هذه التجربة نطبق مثال الفواتير مرتين: مرة كسياسة على الخصائص في OPA مع اختباراتها، ومرة كنموذج علاقات في OpenFGA. نفذنا الأوامر على جهاز فيه Docker Engine 29.8 كما في [دليل تثبيت Docker](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/)، باستخدام ال Docker Images الرسمية `openpolicyagent/opa:1.21.1` و`openfga/openfga:v1.21.0` و`openfga/cli:v0.8.1`، وكل المنافذ مربوطة على `127.0.0.1` فقط، وكل ما ننشئه يبدأ اسمه بالبادئة `lab-authz` حتى نحذفه بسهولة في النهاية.

### الخطوة 1: سياسة الفواتير بلغة Rego

ننشئ مجلداً للتجربة، ونكتب فيه ملف السياسة `policy.rego`:

```bash
mkdir -p ~/authz-lab/opa && cd ~/authz-lab/opa
```

```rego
package invoices

import rego.v1

# Deny by default: anything not allowed below is refused.
default allow := false

user := data.db.users[input.user]

invoice := data.db.invoices[input.invoice]

# 1) The owner of an invoice may read it.
allow if {
	input.action == "read"
	invoice.owner == input.user
}

# 2) Accountants may read the invoices of their own organisation.
allow if {
	input.action == "read"
	"accountant" in user.roles
	user.org == invoice.org
}

# 3) Accountants may approve a draft invoice of their organisation
#    up to their approval limit, and never their own invoice.
allow if {
	input.action == "approve"
	"accountant" in user.roles
	user.org == invoice.org
	invoice.status == "draft"
	invoice.amount <= user.approval_limit
	invoice.owner != input.user
}

# Reasons for a refusal, useful in logs and error messages.
reasons contains "different organisation" if user.org != invoice.org

reasons contains "amount above approval limit" if {
	input.action == "approve"
	invoice.amount > user.approval_limit
}

reasons contains "cannot approve own invoice" if {
	input.action == "approve"
	invoice.owner == input.user
}

# One object the application asks for: the decision and its reasons.
decision := {"allow": allow, "reasons": reasons}
```

ثم ملف البيانات `data.json`، وهو في الواقع ما يأتي من قاعدة بياناتك ومزود الهوية، أي دور PIP، وقد وضعناه في ملف للتبسيط:

```json
{
  "db": {
    "users": {
      "alice": {"org": "acme",   "roles": ["employee"]},
      "bob":   {"org": "acme",   "roles": ["accountant"], "approval_limit": 5000},
      "carol": {"org": "acme",   "roles": ["accountant"], "approval_limit": 50000},
      "dave":  {"org": "globex", "roles": ["accountant"], "approval_limit": 50000}
    },
    "invoices": {
      "42": {"org": "acme", "owner": "alice", "amount": 12000, "status": "draft"},
      "43": {"org": "acme", "owner": "bob",   "amount": 900,   "status": "draft"}
    }
  }
}
```

في الإعداد أعلاه لاحظ التالي:

- `default allow := false` هو تطبيق قاعدة الرفض الافتراضي، فأي طلب لا يطابق إحدى قواعد `allow` مرفوض، ولا يوجد في السياسة أي سطر يقول «ارفض»، والسبب أن نسيان قاعدة يعني رفضاً لا سماحاً.
- القواعد التي تحمل الاسم نفسه `allow` تربط بينها «أو»، والشروط داخل كل قاعدة تربط بينها «و»، فالقاعدة الثالثة لا تسمح إلا إذا تحققت شروطها الستة معاً.
- القاعدة الثالثة هي ما عجز عنه RBAC في [قسم الأدوار](#rbac): الدور `accountant` شرط واحد فيها، ومعه المؤسسة والحالة والحد والملكية، وكلها خصائص.
- جعلنا مفتاح البيانات `db` وليس `invoices` مباشرة، والسبب أن اسم الحزمة `package invoices` يضع السياسة نفسها تحت `data.invoices`، فلو وضعنا البيانات تحت الاسم نفسه لتعارضا، وقد ظهر لنا هذا الخطأ فعلاً في المحاولة الأولى بالرسالة `rego_recursion_error: rule data.invoices.invoice is recursive`.
- `reasons` مجموعة من أسباب الرفض نعيدها مع القرار، فتظهر في رسالة الخطأ للمستخدم وفي سجل القرارات، وهي للشرح فقط، أما القرار نفسه فتحدده `allow`.

### الخطوة 2: نختبر السياسة مثل أي كود

قبل أن نشغل أي سيرفر، نكتب اختبارات للحالات التي نهتم بها في ملف `policy_test.rego`، وكل دالة تبدأ ب `test_` هي اختبار كما في [توثيق الاختبارات في OPA](https://www.openpolicyagent.org/docs/policy-testing?ref=arabroot.io):

```rego
package invoices_test

import rego.v1

import data.invoices

test_owner_can_read if {
	invoices.allow with input as {"user": "alice", "action": "read", "invoice": "42"}
}

test_owner_cannot_approve if {
	not invoices.allow with input as {"user": "alice", "action": "approve", "invoice": "42"}
}

test_other_org_cannot_read if {
	not invoices.allow with input as {"user": "dave", "action": "read", "invoice": "42"}
}

test_limit_blocks_bob if {
	not invoices.allow with input as {"user": "bob", "action": "approve", "invoice": "42"}
}

test_carol_can_approve if {
	invoices.allow with input as {"user": "carol", "action": "approve", "invoice": "42"}
}

test_no_self_approval if {
	not invoices.allow with input as {"user": "bob", "action": "approve", "invoice": "43"}
}
```

ونشغل الاختبارات بالأمر `opa test` من داخل Container مؤقت:

```bash
docker run --rm -v "$PWD:/policies" -w /policies openpolicyagent/opa:1.21.1 test . -v
```

والمخرج سوف يكون كما يلي:

```text
policy_test.rego:11:
data.invoices_test.test_owner_cannot_approve: PASS (12.044954ms)
policy_test.rego:23:
data.invoices_test.test_carol_can_approve: PASS (15.390063ms)
policy_test.rego:19:
data.invoices_test.test_limit_blocks_bob: PASS (15.720083ms)
policy_test.rego:7:
data.invoices_test.test_owner_can_read: PASS (16.066705ms)
policy_test.rego:27:
data.invoices_test.test_no_self_approval: PASS (16.716946ms)
policy_test.rego:15:
data.invoices_test.test_other_org_cannot_read: PASS (37.678757ms)
--------------------------------------------------------------------------------
PASS: 6/6
```

والآن نكسر السياسة عن قصد لنرى قيمة الاختبارات: نفترض أن أحد المطورين حذف السطر `invoice.owner != input.user` من القاعدة الثالثة أثناء تعديل آخر، فأصبح المحاسب يستطيع أن يعتمد فاتورة أنشأها بنفسه، وهذا خرق لفصل المهام Separation of Duties الذي ذكرناه في دليل IAM. وعند تشغيل الاختبارات نفسها على النسخة المعدلة:

```text
policy_test.rego:27:
data.invoices_test.test_no_self_approval: FAIL (20.589296ms)
--------------------------------------------------------------------------------
PASS: 5/6
FAIL: 1/6
```

والأمر يعود برمز الخروج `2`، لذلك إذا شغلته في خط CI كما في دليل [Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/) فإن طلب الدمج الذي يحذف هذا السطر يفشل قبل أن يصل إلى أي سيرفر، وهذه هي الفائدة الحقيقية من كتابة الصلاحيات ككود.

وتستطيع أيضاً أن تقيم السياسة لمدخل واحد دون سيرفر بالأمر `opa eval`، وهو مفيد عند التحقيق في قرار غريب. نكتب المدخل في ملف `input.json` ثم نقيمه:

```bash
echo '{"user":"bob","action":"approve","invoice":"42"}' > input.json
docker run --rm -v "$PWD:/policies" -w /policies openpolicyagent/opa:1.21.1 \
  eval -d policy.rego -d data.json -i input.json --format pretty 'data.invoices.allow'
```

والمخرج سوف يكون كما يلي:

```text
false
```

### الخطوة 3: OPA كخدمة يسألها التطبيق

الآن نشغل OPA كسيرفر، ونحمل فيه السياسة والبيانات، ونفعل طباعة سجل القرارات في المخرج حتى نراه:

```bash
docker run -d --name lab-authz-opa -p 127.0.0.1:18181:8181 \
  -v "$PWD:/policies:ro" openpolicyagent/opa:1.21.1 \
  run --server --addr :8181 --set decision_logs.console=true \
  /policies/policy.rego /policies/data.json
```

والتطبيق، أي PEP، يسأل المحرك عبر [واجهة REST](https://www.openpolicyagent.org/docs/rest-api?ref=arabroot.io) بطلب POST إلى مسار القاعدة التي يريدها، وهنا `/v1/data/invoices/decision`. نجرب خمسة طلبات من خمسة مستخدمين:

```bash
for i in '{"user":"alice","action":"read","invoice":"42"}' \
         '{"user":"alice","action":"approve","invoice":"42"}' \
         '{"user":"dave","action":"read","invoice":"42"}' \
         '{"user":"bob","action":"approve","invoice":"42"}' \
         '{"user":"carol","action":"approve","invoice":"42"}'; do
  echo "> $i"
  curl -s -X POST 127.0.0.1:18181/v1/data/invoices/decision \
    -H 'Content-Type: application/json' -d "{\"input\": $i}"; echo
done
```

والمخرج سوف يكون كما يلي:

```text
> {"user":"alice","action":"read","invoice":"42"}
{"decision_id":"bd161fde-7e95-471f-b2d5-d4100c4883fd","result":{"allow":true,"reasons":[]}}
> {"user":"alice","action":"approve","invoice":"42"}
{"decision_id":"ce83a88d-c47a-43b5-ae5e-01a5ec2231fe","result":{"allow":false,"reasons":["cannot approve own invoice"]}}
> {"user":"dave","action":"read","invoice":"42"}
{"decision_id":"b4fb8252-d64a-4fc9-8b36-68e7df1acaea","result":{"allow":false,"reasons":["different organisation"]}}
> {"user":"bob","action":"approve","invoice":"42"}
{"decision_id":"6ed6a3c8-3bb0-4802-8ea1-76c32218aa5d","result":{"allow":false,"reasons":["amount above approval limit"]}}
> {"user":"carol","action":"approve","invoice":"42"}
{"decision_id":"12c4447a-10d2-4751-81a2-00c10e45b4cc","result":{"allow":true,"reasons":[]}}
```

في المخرج أعلاه لاحظ التالي:

- alice تقرأ فاتورتها لأنها صاحبتها، ولكنها لا تعتمدها، والسبب الذي ظهر هو أنها صاحبتها، رغم أن السبب الأهم أنها ليست محاسبة أصلاً، وهذا يوضح أن قائمة الأسباب تساعدك في الشرح ولكنها ليست القرار، فالقرار هو `allow` وحدها.
- dave محاسب، ولكنه في `globex`، فلا يرى فاتورة `acme`، وهذه هي الحماية من IDOR بين المؤسسات: حتى لو عرف رقم الفاتورة فالمحرك يرفض.
- bob وcarol في الدور نفسه، ومع ذلك القرار مختلف بسبب حد الاعتماد، وهذا ما لا يستطيعه RBAC وحده.
- كل قرار له `decision_id`، وهو الرابط بين رد التطبيق للمستخدم وسطر سجل القرارات.

وسجل القرارات Decision Log هو ما يجيب عن سؤال المدقق، فكل قرار يكتب كسطر JSON فيه المدخل والنتيجة والزمن، وهذا سطر قرار dave من سجل ال Container، وفي الإنتاج Production ترسل هذه السجلات إلى خدمة مركزية كما في [توثيق سجل القرارات](https://www.openpolicyagent.org/docs/management-decision-logs?ref=arabroot.io):

```bash
docker logs lab-authz-opa 2>&1 | grep '"decision_id"' | sed -n 3p | python3 -m json.tool
```

```json
{
    "decision_id": "b4fb8252-d64a-4fc9-8b36-68e7df1acaea",
    "input": {
        "action": "read",
        "invoice": "42",
        "user": "dave"
    },
    "labels": {
        "id": "b608c364-0786-487c-a5ca-a6c76c518d78",
        "version": "1.21.1"
    },
    "level": "info",
    "metrics": {
        "counter_server_query_cache_hit": 1,
        "timer_rego_external_resolve_ns": 800,
        "timer_rego_input_parse_ns": 135008,
        "timer_rego_query_eval_ns": 311818,
        "timer_server_handler_ns": 524131
    },
    "msg": "Decision Log",
    "path": "invoices/decision",
    "req_id": 3,
    "requested_by": "172.17.0.1:57392",
    "result": {
        "allow": false,
        "reasons": [
            "different organisation"
        ]
    },
    "time": "2026-10-06T00:11:04Z",
    "timestamp": "2026-10-06T00:11:04.437268038Z",
    "type": "openpolicyagent.org/decision_logs"
}
```

لاحظ في الحقل `metrics` أن تقييم السياسة نفسه `timer_rego_query_eval_ns` استغرق نحو 0.31 مللي ثانية، ومعالجة الطلب كاملة داخل OPA نحو 0.52 مللي ثانية، وهذه أرقام من جهازنا في هذه التجربة الصغيرة للتوضيح فقط، والزمن الذي يهمك في الإنتاج هو زمن الشبكة بين التطبيق والمحرك، وهو سبب تشغيل OPA ك Sidecar.

والآن نغير البيانات لا السياسة: رفعت الإدارة حد الاعتماد عند bob إلى 20000، فنحدث البيانات عبر واجهة البيانات Data API، ثم نسأل السؤال نفسه:

```bash
curl -s -o /dev/null -w '%{http_code}\n' -X PUT \
  127.0.0.1:18181/v1/data/db/users/bob/approval_limit \
  -H 'Content-Type: application/json' -d '20000'
curl -s -X POST 127.0.0.1:18181/v1/data/invoices/decision \
  -H 'Content-Type: application/json' \
  -d '{"input":{"user":"bob","action":"approve","invoice":"42"}}'; echo
```

والمخرج سوف يكون كما يلي:

```text
204
{"decision_id":"902baabd-3d4a-4140-bd84-a8a862a04e3d","result":{"allow":true,"reasons":[]}}
```

القرار تغير دون أن يتغير سطر واحد في السياسة أو في التطبيق، لأن الحد خاصية في البيانات. وفي الإنتاج لا تحدث البيانات يدوياً بهذا الشكل، وإنما تصل مع الحزمة أو يرسلها التطبيق مع المدخل، والمهم أن تعرف أن هناك مصدرين للقرار: السياسة التي تتغير بطلب دمج ومراجعة، والبيانات التي تتغير مع عمل المؤسسة.

### الخطوة 4: نموذج العلاقات في OpenFGA

الآن نعبر عن جزء المشاركة من المثال كعلاقات. نشغل OpenFGA بقاعدة بيانات في الذاكرة، وهي مناسبة للتجربة فقط لأن كل شيء يختفي عند إيقافه، وللإنتاج تستخدم PostgreSQL كما في [دليل التشغيل الرسمي](https://openfga.dev/docs/getting-started/setup-openfga/docker?ref=arabroot.io). ونضع المحرك وأداة سطر الأوامر في شبكة واحدة:

```bash
mkdir -p ~/authz-lab/fga && cd ~/authz-lab/fga
docker network create lab-authz-net
docker run -d --name lab-authz-openfga --network lab-authz-net \
  -p 127.0.0.1:18080:8080 openfga/openfga:v1.21.0 run

# fga CLI from its official image, talking to the server on the same network
fga() { docker run --rm --network lab-authz-net -v "$PWD:/work" -w /work \
  -e FGA_API_URL=http://lab-authz-openfga:8080 -e FGA_STORE_ID="$FGA_STORE_ID" \
  openfga/cli:v0.8.1 "$@"; }
```

ثم نكتب النموذج في ملف `model.fga`:

```text
model
  schema 1.1

type user

type team
  relations
    define member: [user]

type folder
  relations
    define owner: [team#member]
    define viewer: [user, team#member] or owner

type document
  relations
    define parent: [folder]
    define owner: [user]
    define editor: [user] or owner or owner from parent
    define viewer: [user] or editor or viewer from parent
```

في الإعداد أعلاه لاحظ التالي:

- كل `type` نوع من الكائنات، وكل `define` علاقة، وما بين الأقواس المربعة هو من يمكن أن يرتبط بهذه العلاقة مباشرة، فمالك المجلد `owner` لا يكون إلا أعضاء فريق `team#member`، وليس مستخدماً منفرداً.
- `or owner` تعني أن كل مالك هو أيضاً قارئ، فلا تحتاج إلى أن تكتب العلاقتين.
- `viewer from parent` هي الوراثة: من يقرأ المجلد الأب يقرأ المستند، و`owner from parent` تجعل مالك المجلد محرراً لكل ما بداخله، وهذا بالضبط ما وصفته ورقة Zanzibar بالمستند الذي يرث صلاحيات مجلده.
- لا يوجد في النموذج أي مستخدم ولا أي فاتورة، فهو يصف الأنواع والقواعد فقط، والبيانات تأتي كعلاقات Tuples في الخطوة التالية، وهذا هو دور PAP في هذا المحرك.

ننشئ مخزناً Store ونحمل فيه النموذج:

```bash
fga store create --name invoices --model model.fga
```

والمخرج سوف يكون كما يلي:

```json
{"store":{"created_at":"2026-10-05T23:39:19.618128698Z","id":"01M476Z8P21CJ9MJRY1AAH8P81","name":"invoices","updated_at":"2026-10-05T23:39:19.618128698Z"},"model":{"authorization_model_id":"01M476Z8P961G5SF62M72XV4Q5"}}
```

ونحفظ معرف المخزن في متغير حتى تستخدمه بقية الأوامر:

```bash
export FGA_STORE_ID=01M476Z8P21CJ9MJRY1AAH8P81
```

### الخطوة 5: نكتب العلاقات ونسأل المحرك

نكتب العلاقات في ملف `tuples.yaml`: alice عضو في فريق المالية، وفريق المالية يملك مجلد فواتير 2026، والفاتورة 42 داخل هذا المجلد، وcarol تقرأ الفاتورة 43 مباشرة:

```yaml
- user: user:alice
  relation: member
  object: team:finance
- user: team:finance#member
  relation: owner
  object: folder:invoices-2026
- user: folder:invoices-2026
  relation: parent
  object: document:invoice-42
- user: user:carol
  relation: viewer
  object: document:invoice-43
```

```bash
fga tuple write --file tuples.yaml
```

والمخرج سوف يكون كما يلي:

```json
{"failed_count":0,"successful":[{"object":"team:finance","relation":"member","user":"user:alice"},{"object":"folder:invoices-2026","relation":"owner","user":"team:finance#member"},{"object":"document:invoice-42","relation":"parent","user":"folder:invoices-2026"},{"object":"document:invoice-43","relation":"viewer","user":"user:carol"}],"successful_count":4,"time_spent":"15.250891ms","total_count":4}
```

الآن نسأل المحرك بالأمر `check`، وهو ما يفعله التطبيق عند كل طلب:

```bash
fga query check user:alice viewer document:invoice-42
fga query check user:alice editor document:invoice-42
fga query check user:bob viewer document:invoice-42
fga query list-objects user:alice viewer document
```

والمخرج سوف يكون كما يلي:

```json
{"allowed":true,"resolution":""}
{"allowed":true,"resolution":""}
{"allowed":false,"resolution":""}
{"objects":["document:invoice-42"]}
```

لاحظ أنه لا يوجد في كل ما كتبناه سطر يقول «alice تقرأ الفاتورة 42»، والمحرك استنتج ذلك بالمشي في الرسم البياني للعلاقات كما تبين الصورة التالية، ولاحظ أن alice محررة أيضاً، لأن فريقها يملك المجلد والنموذج يقول إن مالك المجلد يحرر ما بداخله، فإذا لم يكن هذا ما تريده فالخطأ في النموذج وليس في المحرك، وهذا سبب آخر لاختبار النموذج كما سوف نرى. وأما الأمر الأخير `list-objects` فهو السؤال العكسي الذي تحتاجه صفحة «فواتيري»: ما المستندات التي تستطيع alice قراءتها؟

![مخطط للسؤال check(user:alice, viewer, document:invoice-42): أربع عقد من اليمين إلى اليسار هي user:alice وteam:finance وfolder:invoices-2026 وdocument:invoice-42، وتربطها العلاقات member وowner وparent مع صيغة كل علاقة مثل team:finance#member@user:alice. وأسهم مرقمة تمشي من المستند إلى المجلد إلى الفريق إلى alice: 1 قارئ المستند هو قارئ مجلده، 2 قارئ المجلد يشمل مالكه team:finance#member، 3 هل alice عضو في team:finance؟ نعم، والنتيجة allowed true. وفي الأسفل bob بلا مسار فالنتيجة allowed false، ثم بعد fga tuple write user:bob member team:finance تصبح allowed true دون تغيير كود أو نشر](https://arabroot.io/content/images/2026/10/authorization-04-rebac-graph.webp)

المحرك يجيب بالمشي من المستند إلى المستخدم عبر العلاقات، وعلاقة واحدة جديدة تكفي لتغيير القرار

والآن ينضم bob إلى فريق المالية. في نظام الشروط المتفرقة كنت ستبحث عن كل مكان يقرر من يرى الفواتير، أما هنا فهي علاقة واحدة:

```bash
fga tuple write user:bob member team:finance
fga query check user:bob viewer document:invoice-42
fga query list-objects user:bob editor document
```

والمخرج سوف يكون كما يلي:

```json
{"successful":[{"object":"team:finance","relation":"member","user":"user:bob"}]}
{"allowed":true,"resolution":""}
{"objects":["document:invoice-42"]}
```

وعندما يغادر bob الفريق نحذف العلاقة، فيسحب منه الوصول في الطلب التالي مباشرة، وهذا ما تريده تماماً عندما يصل حدث المغادرة من SCIM:

```bash
fga tuple delete user:bob member team:finance
fga query check user:bob viewer document:invoice-42
fga query list-users --object document:invoice-42 --relation viewer --user-filter user
```

والمخرج سوف يكون كما يلي:

```json
{}
{"allowed":false,"resolution":""}
{"users":[{"object":{"id":"alice","type":"user"}}]}
```

والأمر الأخير `list-users` هو جواب سؤال مدير المالية في بداية المقال: «من يستطيع رؤية هذه الفاتورة؟»، والإجابة الآن alice وحدها، وهي إجابة يعطيها المحرك لا يستنتجها أحد من قراءة الكود.

والتطبيق في الإنتاج لا يستخدم أداة سطر الأوامر، وإنما يرسل الطلب نفسه إلى واجهة HTTP، وهنا تستطيع أن تحدد مستوى الاتساق لكل طلب، وسوف نشرح معناه بعد قليل:

```bash
curl -s -X POST http://127.0.0.1:18080/stores/$FGA_STORE_ID/check \
  -H 'Content-Type: application/json' \
  -d '{"tuple_key":{"user":"user:alice","relation":"viewer","object":"document:invoice-42"},"consistency":"HIGHER_CONSISTENCY"}'
```

```json
{"allowed":true,"resolution":""}
```

### ماذا يحدث إذا كتبت علاقة لا يسمح بها النموذج؟

النموذج يقول إن مالك المجلد لا يكون إلا أعضاء فريق، فلنحاول أن نجعل bob مالكاً للمجلد مباشرة:

```bash
fga tuple write user:bob owner folder:invoices-2026
```

```text
Error: failed to write tuple: POST validation error for Write POST with body {"code":"validation_error","message":"Invalid tuple 'folder:invoices-2026#owner@user:bob'. Reason: type 'user' is not an allowed type restriction for 'folder#owner'"}
 with error code validation_error error message: Invalid tuple 'folder:invoices-2026#owner@user:bob'. Reason: type 'user' is not an allowed type restriction for 'folder#owner'
```

المحرك رفض العلاقة لأنها تخالف النموذج، والرسالة تقول ذلك بوضوح: النوع `user` غير مسموح في `folder#owner`. وهذه حماية مفيدة، لأن النموذج يمنع أن تتسلل صلاحيات فردية إلى مكان صممته للفرق، فتبقى المراجعة ممكنة.

### الخطوة 6: نختبر النموذج أيضاً

النموذج كود مثل السياسة، ويختبر بملف يجمع النموذج وعلاقات تجريبية والنتائج المتوقعة، كما في [توثيق اختبار النماذج](https://openfga.dev/docs/modeling/testing?ref=arabroot.io). نكتب الملف `store.fga.yaml`:

```yaml
name: invoices
model_file: ./model.fga

tuples:
  - user: user:alice
    relation: member
    object: team:finance
  - user: team:finance#member
    relation: owner
    object: folder:invoices-2026
  - user: folder:invoices-2026
    relation: parent
    object: document:invoice-42

tests:
  - name: team members inherit access from the folder
    check:
      - user: user:alice
        object: document:invoice-42
        assertions:
          viewer: true
          editor: true
      - user: user:bob
        object: document:invoice-42
        assertions:
          viewer: false
    list_objects:
      - user: user:alice
        type: document
        assertions:
          viewer:
            - document:invoice-42
```

```bash
fga model test --tests store.fga.yaml
```

والمخرج سوف يكون كما يلي:

```text
# Test Summary #
Tests 1/1 passing
Checks 3/3 passing
ListObjects 1/1 passing
```

هذا الاختبار لا يحتاج إلى السيرفر، فيعمل في خط CI مثل `opa test`، وأي تعديل في النموذج يوسع صلاحية دون قصد يكسر أحد التوقعات قبل النشر.

### إزالة التجربة

نحذف فقط ما أنشأناه، بالاسم:

```bash
docker rm -f lab-authz-opa lab-authz-openfga
docker network rm lab-authz-net
rm -rf ~/authz-lab
```

⚠️

لا تستخدم أوامر التنظيف العامة مثل `docker system prune` أو `docker volume prune` لإزالة تجربة، لأنها تحذف كل ما هو متوقف أو غير مرتبط على الجهاز، ومنه Volumes لمشاريع أخرى قد تكون متوقفة مؤقتاً. احذف ما أنشأته باسمه أو ببادئته فقط.

## الصلاحيات ككود في البنية التحتية: Kubernetes

الأفكار نفسها تطبق على البنية التحتية، ومن يعمل مع Kubernetes سوف يقابل ثلاث طبقات:

**RBAC في Kubernetes:** واجهة Kubernetes نفسها تستخدم الصلاحيات حسب الدور عبر أربعة أنواع من الكائنات كما في [التوثيق الرسمي](https://kubernetes.io/docs/reference/access-authn-authz/rbac/?ref=arabroot.io): `Role` لصلاحيات داخل Namespace واحد و`ClusterRole` لصلاحيات على مستوى العنقود، و`RoleBinding` و`ClusterRoleBinding` لربطها بمستخدم أو مجموعة أو Service Account. والصلاحيات فيها جمعية Additive فقط، أي لا توجد قواعد رفض، فكل ما لم يمنح فهو مرفوض، وتستطيع أن تسأل عن قرار بعينه بالأمر `kubectl auth can-i`. وهذه الطبقة تجيب عن «من يستطيع إنشاء Pod في هذا ال Namespace؟»، ولكنها لا تستطيع أن تقول «ولكن لا تسمح بأي Pod يعمل بصلاحيات root».

**OPA Gatekeeper:** هنا يأتي التحكم بالقبول Admission Control، أي فحص محتوى الكائن قبل أن يحفظ. و[Gatekeeper](https://open-policy-agent.github.io/gatekeeper/website/docs/?ref=arabroot.io) مشروع تابع ل OPA يعمل ك Webhook للقبول، والسياسات فيه بلغة Rego داخل كائنات Kubernetes من نوع `ConstraintTemplate` تعرف القاعدة و`Constraint` تطبقها بمعاملات، فتستطيع أن تمنع مثلاً أي Docker Image لا يأتي من ال Registry الخاص بك، وفيه وظيفة تدقيق Audit تكشف الكائنات الموجودة أصلاً التي تخالف السياسة.

**Kyverno:** و[Kyverno](https://kyverno.io/docs/introduction/?ref=arabroot.io) يحل المشكلة نفسها دون لغة جديدة، فالسياسات كائنات Kubernetes بصيغة YAML مع تعبيرات CEL، ويستطيع أن يتحقق من الكائنات ويعدلها وينشئ كائنات جديدة ويتحقق من توقيع ال Docker Images، وقد وصل إلى مستوى Graduated في CNCF في 16 مارس 2026 كما في [صفحته في CNCF](https://www.cncf.io/projects/kyverno/?ref=arabroot.io). والاختيار بين الاثنين في الغالب سؤال لغة: إذا كان فريقك يكتب Rego أصلاً للتطبيقات فGatekeeper يوحد اللغة، وإذا كان يفضل YAML فKyverno أسهل عليه.

وأما صلاحيات السيرفرات نفسها، مثل sudo وأقل صلاحية ممكنة للمديرين وشهادات SSH قصيرة العمر، فقد شرحناها في دليل [إدارة الهوية والوصول IAM والوصول المميز PAM](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/).

## كيف تصمم الصلاحيات في نظامك؟

بعد أن رأيت النماذج والمحركات، فيما يلي القواعد التي تجعل التصميم صحيحاً مهما كان المحرك الذي تختاره:

1. **ابدأ بأدوار قليلة، وأضف الخصائص والعلاقات عند الحاجة:** ثلاثة أو أربعة أدوار واضحة تكفي أغلب التطبيقات في البداية، وعندما تجد نفسك تنشئ دوراً جديداً لكل مؤسسة أو لكل حد مبلغ فهذه علامة أنك تحتاج إلى خاصية، وعندما تحتاج إلى مشاركة مجلد مع فريق فهذه علامة أنك تحتاج إلى علاقة.
2. **ارفض افتراضياً:** كل ما لم تسمح به قاعدة صريحة مرفوض، وإذا لم يرد المحرك أو حدث خطأ في الاتصال به فالجواب رفض، والسبب أن الخطأ في اتجاه الرفض يزعج مستخدماً، أما الخطأ في اتجاه السماح فيكشف بيانات.
3. **افحص على السيرفر في كل طلب، ولا تثق بالواجهة:** إخفاء زر «اعتماد» في الواجهة تحسين لتجربة المستخدم وليس حماية، فأي شخص يستطيع أن يرسل الطلب مباشرة بأداة مثل curl، لذلك يجب أن يسأل السيرفر المحرك عند كل طلب، وتستطيع الواجهة أن تسأله أيضاً لتقرر ما تعرضه، ولكن القرار الذي يحمي هو قرار السيرفر.
4. **سجل القرارات، لا الطلبات فقط:** سجل كل قرار بالمستخدم والفعل والمورد والنتيجة والسبب، كما رأيت في سجل OPA، وراقب الرفض المتكرر لأنه غالباً شخص يجرب أرقام الفواتير.
5. **اختبر الصلاحيات مثل أي كود:** لكل قاعدة اختبار يسمح واختبار يرفض، والاختبارات تعمل في CI عند كل طلب دمج، وتغيير السياسة يمر بمراجعة شخص ثان مثل أي كود حساس.
6. **اجعل الهوية في التوكن والقرار في المحرك:** التوكن يحمل من هو المستخدم ومجموعاته العامة، والمحرك يقرر الصلاحية على كل مورد.

والصورة التالية تبين هذه الدورة الكاملة للسياسة من كتابتها إلى مراجعة قراراتها:

![دورة مرقمة حول مستودع Git واحد للسياسات: 1 write كتابة policy.rego أو model.fga، 2 test بالأمرين opa test وfga model test، 3 review طلب دمج بمراجع ثان، 4 deploy نشر الحزمة إلى كل PDP، 5 decide قرار allow أو deny لكل طلب، 6 audit سجلات القرارات والمراجعات، ثم العودة إلى الكتابة](https://arabroot.io/content/images/2026/10/authorization-06-lifecycle.webp)

السياسة تكتب وتختبر وتراجع وتنشر مثل أي كود، وسجل القرارات يخبرك متى تحتاج إلى تعديلها

### الاتساق ومشكلة «العدو الجديد»

عندما يكون المحرك خدمة موزعة تحفظ العلاقات على أكثر من سيرفر، أو عندما يستخدم التخزين المؤقت، تظهر مشكلة دقيقة سمتها ورقة Zanzibar مشكلة «العدو الجديد» New Enemy Problem، ومثالها في الورقة: تحذف alice شخصاً اسمه bob من قائمة الوصول لمجلد، ثم تطلب من زميل أن يضع مستندات جديدة في المجلد، فإذا قيم فحص الصلاحية على نسخة قديمة من العلاقات لم تصلها عملية الحذف بعد، فإن bob يرى المستندات الجديدة التي أضيفت بعد حذفه بالذات. والسبب أن المحرك لم يحترم ترتيب الأحداث، وهو ما شرحناه بشكل عام في دليل [الأنظمة الموزعة ونماذج الاتساق](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-cap-%D9%88%D8%A7%D9%84%D8%A7%D8%AA%D8%B3%D8%A7%D9%82-%D9%88%D8%A7%D9%84%D8%A7%D8%AC%D9%85%D8%A7%D8%B9/#consistency-models).

وحل Zanzibar رمز صغير اسمه Zookie: عندما يحفظ التطبيق تعديلاً على المحتوى يطلب من المحرك رمزاً فيه طابع زمني يضمن أن كل تغييرات الصلاحيات السابقة قبله، ويحفظه مع المحتوى، ثم يرسله في كل فحص لاحق لهذا المحتوى، فيقيم المحرك الفحص على بيانات لا تقل حداثة عن هذا الرمز. وفي SpiceDB اسم هذا الرمز ZedToken، وتختار مستوى الاتساق لكل طلب كما في [توثيق الاتساق](https://authzed.com/docs/spicedb/concepts/consistency?ref=arabroot.io). وفي OpenFGA الخيار أبسط كما في [توثيقه](https://openfga.dev/docs/interacting/consistency?ref=arabroot.io): `MINIMIZE_LATENCY` وهو الافتراضي ويخدم من الذاكرة المؤقتة إذا كانت مفعلة، و`HIGHER_CONSISTENCY` الذي يتجاوزها ويقرأ من قاعدة البيانات مباشرة، والتوثيق نفسه ينبه إلى أنك إذا كتبت علاقة ثم فحصت مباشرة بالخيار الأول فقد لا يظهر التغيير، لذلك استخدم الخيار الثاني بعد سحب صلاحية أو في العمليات الحساسة، والأول في الباقي.

### الانتقال من الشروط المتفرقة إلى محرك

لا تحاول أن تنقل كل الصلاحيات في يوم واحد، لأن أي اختلاف بين القديم والجديد سوف يظهر كخطأ عند المستخدمين، والطريقة الآمنة كما يلي:

1. **اجرد الشروط الموجودة:** ابحث في الكود عن كل `if` يقارن دوراً أو مالكاً أو مؤسسة، واكتبها كقواعد بلغة بشرية، وغالباً سوف تجد في هذه الخطوة تناقضات وثغرات قبل أن تكتب سطراً واحداً.
2. **اجمعها خلف دالة واحدة:** استبدل كل الشروط في التطبيق باستدعاء دالة واحدة مثل `authorize(user, action, resource)` تحتوي المنطق القديم كما هو، فيصبح لديك مكان واحد تستبدله لاحقاً.
3. **اكتب السياسة واختباراتها:** انقل القواعد إلى المحرك، واكتب لكل قاعدة اختبار سماح واختبار رفض.
4. **شغل الوضع الظلي Shadow Mode:** اجعل الدالة تسأل المحرك أيضاً، وتسجل كل حالة يختلف فيها قراره عن المنطق القديم، دون أن تستخدم قراره بعد. وبعد أسبوع أو اثنين من حركة حقيقية، كل اختلاف إما خطأ في السياسة الجديدة أو ثغرة في الكود القديم.
5. **بدل ثم احذف:** عندما تختفي الاختلافات اجعل قرار المحرك هو المعتمد، ثم احذف المنطق القديم حتى لا يعود أحد لاستخدامه.

## الأخطاء الشائعة

- **الاعتماد على البوابة وحدها:** Forward Auth يحمي التطبيق من الغرباء، ولكنه لا يمنع مستخدماً مسجلاً من قراءة فاتورة مؤسسة أخرى، لذلك يبقى الفحص الدقيق داخل التطبيق.
- **الفحص في الواجهة فقط:** إخفاء الأزرار والروابط لا يحمي أي شيء، لأن الطلب يمكن أن يرسل مباشرة.
- **اعتبار ال Scope صلاحية:** `invoices:read` في التوكن يعني أن التطبيق العميل مسموح له بالقراءة نيابة عن المستخدم، ولا يعني أن المستخدم يقرأ كل الفواتير.
- **كل الصلاحيات في JWT:** توكن كبير وقديم بعد أول تغيير، ولا يمكن سحب الصلاحية منه قبل انتهائه.
- **السماح عند فشل المحرك:** كتابة `except: return True` حول استدعاء المحرك حتى «لا يتوقف العمل» تحول أي عطل في الشبكة إلى ثغرة تفتح كل شيء.
- **فحص القائمة دون فحص العنصر:** ترشيح صفحة «فواتيري» صحيح، ثم ينسى المطور الفحص في مسار `/invoices/{id}` أو في التصدير، وهذا بالضبط شكل IDOR.
- **علاقات غير متزامنة:** نقل الفاتورة إلى مجلد آخر في قاعدة البيانات دون تحديث العلاقة في محرك ReBAC، فيبقى القرار مبنياً على المكان القديم.
- **أدوار بلا نهاية:** دور جديد لكل استثناء، حتى لا يعرف أحد ما الفرق بين `accountant-2` و`accountant-new`.
- **سياسة بلا اختبارات ولا مراجعة:** تعديل صغير يحذف شرطاً، كما رأيت في التجربة، ولا يكتشفه أحد حتى يستغله شخص.

## قائمة تحقق

- هل تستطيع أن تجيب اليوم عن «من يستطيع رؤية المورد X، ولماذا؟» بأمر أو استعلام، لا بقراءة الكود؟
- هل القاعدة مكتوبة في مكان واحد، وكل الخدمات والواجهات والتصدير تسأل هذا المكان؟
- هل الرفض هو الافتراضي، ومنه حالة تعطل المحرك؟
- هل يفحص السيرفر كل طلب لمورد بعينه، ومنها مسارات التصدير والتنزيل والواجهات الداخلية؟
- هل للسياسة اختبارات سماح ورفض تعمل في CI، ومراجعة من شخص ثان قبل الدمج؟
- هل التوكن يحمل الهوية والمجموعات فقط، والقرار الدقيق في المحرك؟
- هل يسجل كل قرار بالمستخدم والفعل والمورد والنتيجة، ومع تنبيه على الرفض المتكرر؟
- إذا كنت تستخدم علاقات: هل يتحدث المحرك عند كل تغيير في البيانات، وهل تستخدم اتساقاً أعلى بعد سحب الصلاحيات؟
- هل راجعت ترخيص المحرك وحالته ومن يقف خلفه قبل أن تبني عليه؟

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- المصادقة تخبرك من هو المستخدم، والتفويض يقرر ماذا يفعل بكل مورد عند كل طلب، والشروط المتفرقة في الكود تعطي قواعد مختلفة في كل خدمة وثغرات IDOR، وهي جزء من فئة Broken Access Control التي بقيت الأولى في OWASP Top 10 في إصداري 2021 و2025.
- ACL قائمة على كل كائن، وRBAC أدوار سهلة المراجعة تنفجر مع الاستثناءات، وABAC قاعدة على خصائص المستخدم والمورد والسياق، وReBAC مسار علاقات بدأ من ورقة Google Zanzibar عام 2019، وأغلب الأنظمة تبدأ بالأدوار وتضيف الباقي عند الحاجة.
- التطبيق PEP يسأل وينفذ، والمحرك PDP يقرر ويسجل، والسياسات تكتب في PAP والحقائق تأتي من PIP، والبوابة تفحص فحصاً عاماً والقرار الدقيق داخل التطبيق.
- ال Scope تفويض للتطبيق العميل، والمجموعات في التوكن مدخل للقرار، ولا تضع كل الصلاحيات في JWT لأنها تكبر وتصبح قديمة.
- OPA وCedar وCerbos للسياسات، وOpenFGA وSpiceDB وOry Keto وPermify للعلاقات، وCasbin مكتبة داخل التطبيق، وكلها Apache-2.0 عدا Permify بترخيص AGPL-3.0، وTopaz يستمر دون شركته.
- في التجربة رأيت سياسة Rego ترفض الاعتماد فوق الحد وتسجل السبب، واختباراً يفشل عندما يحذف شرط فصل المهام، ونموذج OpenFGA يعطي bob الوصول بعلاقة واحدة ويسحبه بحذفها، ويجيب عن «من يستطيع رؤية هذه الفاتورة؟» بأمر واحد.

[![](https://arabroot.io/content/images/2026/10/iam-pam-cover-cover.webp)دليلإدارة الهوية والوصول IAM والوصول المميز PAMمن يملك صلاحية root على سيرفراتك ومنذ متى، وكيف تضبط حسابات المديرين بصلاحيات مؤقتة وجلسات مسجلة.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/)[![](https://arabroot.io/content/images/2026/10/oauth-oidc-cover-cover.webp)دليلشرح OAuth وOpenID Connect للمبتدئينلماذا لا يجب أن يعرف أي تطبيق كلمة مرورك، وكيف يحل OAuth ذلك بتوكنات محدودة، وكيف يضيف OpenID Connect الدخول الموحد.](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-oauth-%D9%88-openid-connect-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/sessions-cookies-jwt-cover-cover.webp)دليلكيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWTبروتوكول HTTP لا يتذكر أحداً، فكيف يبقى المستخدم مسجلاً بين الصفحات، ومتى تختار الجلسة على السيرفر ومتى تختار JWT.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/scim-cover-cover.webp)دليلما هو SCIM؟ إدارة حسابات الموظفين تلقائياً في كل تطبيقاتكالدخول الموحد SSO يمنع الموظف المغادر من الدخول لكن حساباته تبقى نشطة، وSCIM ينشئ الحسابات ويعطلها في كل تطبيق تلقائياً.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-scim-%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D8%AD%D8%B3%D8%A7%D8%A8%D8%A7%D8%AA-%D8%AA%D9%84%D9%82%D8%A7%D8%A6%D9%8A%D8%A7/)[![](https://arabroot.io/content/images/2026/10/authentik-forward-auth-cover-cover.webp)دليلكيف تحمي أي تطبيق ويب بصفحة دخول authentik عبر Forward Authكيف تضع صفحة دخول authentik مع المصادقة الثنائية أمام أي أداة داخلية دون أن تعدل في التطبيق نفسه.](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على OPA 1.21.1 وOpenFGA 1.21.0 مع fga CLI 0.8.1 على Docker Engine 29.8، ومراجعة تراخيص المحركات وحالتها من مستودعاتها الرسمية وصفحات CNCF.
- أكتوبر 2026: إضافة رابط إلى سلسلة المقالات التالية: منصة الصلاحيات المركزية، وOpenFGA عملياً، وOPA عملياً.