لنفرض أن لديك تطبيقاً للفواتير، فيه واجهة ويب وواجهة API لتطبيق الجوال وخدمة تصدر ملفات PDF ولوحة إدارة صغيرة، وقد ربطت الدخول بمزود هوية كما شرحنا في دليل OAuth وOpenID Connect، فأصبح كل تطبيق يعرف بثقة من هو المستخدم الذي أرسل الطلب. ثم يأتي مدير المالية بسؤال يبدو بسيطاً: من يستطيع اليوم أن يرى الفاتورة رقم 42، ومن يستطيع أن يعتمدها، ولماذا؟ وعندما تبحث عن الإجابة تجد أنها ليست في مكان واحد، فهي موزعة على عشرات الشروط من نوع if في أربع خدمات كتبها أربعة مطورين في أوقات مختلفة، وكل خدمة تفهم القاعدة بطريقتها، وإحداها لا تتحقق من شيء أصلاً.
وهذا هو موضوع هذا المقال: التفويض Authorization، أي الإجابة عن سؤال «ماذا يحق لهذا المستخدم أن يفعل بهذا المورد الآن؟»، وكيف تنقله من شروط متفرقة في الكود إلى مكان واحد واضح يمكن قراءته واختباره ومراجعته. وقد عرفنا في دليل إدارة الهوية والوصول IAM نموذجي 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 داخل الدالة التي تعالج الطلب، يقارن دور المستخدم أو معرفه بحقل في السجل. وهذه الطريقة تعمل في البداية، ولكن مع نمو النظام تظهر مشاكلها كما تبين الصورة التالية:

في الصورة أعلاه لاحظ التالي:
- القاعدة نفسها مكتوبة أربع مرات وبأربعة أشكال: واجهة الويب تسمح للمدير ولصاحب الفاتورة، وواجهة API تسمح لكل من في المؤسسة نفسها لأن المطور نسي شرط المالك، ولوحة الإدارة تثق بنطاق البريد الإلكتروني، وكل تعديل في القاعدة يحتاج إلى أن تتذكر الأماكن الأربعة، وغالباً سوف تنسى واحداً منها.
- لا أحد يستطيع الإجابة عن «من يستطيع رؤية الفاتورة 42، ولماذا؟»: لأن الإجابة ليست بيانات يمكن الاستعلام عنها، وإنما سلوك موزع في الكود، والطريقة الوحيدة لمعرفته هي قراءة كل الكود.
- المراجعة والتدقيق Audit شبه مستحيلين: فعندما يسأل المدقق «من اعتمد هذه الفاتورة، وعلى أي أساس سمح له النظام؟» لا يوجد سجل للقرار، وإنما سجل للطلب في أحسن الأحوال.
- شرط واحد منسي يكفي لثغرة: خدمة التصدير تأخذ رقم الفاتورة من الرابط وتعيد الملف دون أن تسأل لمن هو، وكل ما يحتاجه المخترق هو أن يغير الرقم في الرابط.
وهذه الثغرة الأخيرة لها اسم معروف هو الإشارة المباشرة غير الآمنة إلى الكائن Insecure Direct Object Reference واختصاراً IDOR، وقد شرحنا في دليل المعرفات الفريدة لماذا لا يحميك المعرف العشوائي منها، فالمعرف ليس صلاحية، والحماية الوحيدة هي فحص التفويض في كل طلب. وهي جزء من فئة أوسع تضعها OWASP في المرتبة الأولى بين أخطر مخاطر تطبيقات الويب، وهي تعطل التحكم بالوصول Broken Access Control: ففي إصدار 2021 صعدت هذه الفئة من المرتبة الخامسة إلى الأولى، كما في صفحة A01:2021، وفي إصدار 2025 حافظت على المرتبة الأولى، وتذكر صفحة A01:2025 أن كل التطبيقات التي اختبرت فيها وجد فيها شكل من أشكال هذا الخلل، وأن الفئة تضم 40 نوعاً من الأخطاء CWE، ومن أمثلتها المذكورة صراحة عرض حساب شخص آخر أو تعديله بمجرد تغيير معرفه، ومن أول توصياتها: ارفض افتراضياً Deny by Default كل ما ليس عاماً، وسجل حالات الرفض ونبه المديرين عند تكرارها.
الحلول الأولى التي تخطر على البال، ولماذا لا تكفي
عندما يشعر الفريق بالمشكلة، فإن أول ما يفعله عادة هو أحد الحلول التالية، وكل واحد منها خطوة في الاتجاه الصحيح ولكنه يتوقف في منتصف الطريق:
- دالة مساعدة مشتركة: يجمع الفريق الشروط في دالة واحدة مثل
can_approve(user, invoice)داخل التطبيق، وهذا أفضل بكثير من الشروط المتفرقة، ولكنه يعمل فقط ما دام التطبيق واحداً ومكتوباً بلغة واحدة. فعندما تكتب خدمة التصدير بلغة Go وواجهة الويب بلغة Python، تعود القاعدة لتكتب مرتين، وتعود النسختان لتختلفا مع الوقت. - مكتبة مشتركة بين الخدمات: تنقل الدالة إلى مكتبة تستوردها كل خدمة، فتواجه مشكلة الإصدارات: الخدمة التي لم تحدث المكتبة منذ ثلاثة أشهر ما زالت تطبق القاعدة القديمة، ولا تعرف ذلك إلا عندما يحدث خطأ.
- وضع كل الصلاحيات في التوكن: يضيف الفريق إلى JWT قائمة بكل ما يحق للمستخدم، فيكبر التوكن ويصبح قديماً بمجرد أن تتغير صلاحية، وسوف نناقش هذا الحل بالتفصيل في قسم التوكنات لأنه شائع جداً.
والحل الأنسب هو أن تفصل القرار عن الكود: القاعدة تكتب مرة واحدة في مكان واحد، بلغة مخصصة لها، ويسأل كل تطبيق هذا المكان عند كل طلب، وهذا ما تفعله محركات الصلاحيات التي سوف نراها. ولكن قبل المحركات نحتاج إلى أن نتفق على طريقة التفكير في الصلاحيات نفسها، أي النموذج.
نماذج الصلاحيات على مثال واحد: الفواتير
سوف نستخدم في بقية المقال مثالاً واحداً حتى تقارن النماذج على الأرض نفسها. لدينا مؤسستان acme وglobex تستخدمان التطبيق نفسه، وفي acme موظفة اسمها alice أنشأت الفاتورة 42 بمبلغ 12000، ومحاسبان هما bob وحد الاعتماد عنده 5000 وcarol وحدها 50000، وفي globex محاسب اسمه dave. والفواتير محفوظة في مجلدات، وكل مجلد يملكه فريق، مثل فريق المالية finance. والصورة التالية تبين كيف يجيب كل نموذج عن السؤال نفسه:

قائمة التحكم بالوصول ACL: قائمة على كل كائن
قائمة التحكم بالوصول Access Control List واختصاراً ACL هي أقدم النماذج وأبسطها: كل كائن تلحق به قائمة تقول من يستطيع أن يفعل ماذا، فالفاتورة 42 لها قائمة فيها alice: read, write وcarol: read, approve. وهذا ما تراه في صلاحيات الملفات في Linux وفي مشاركة ملف مع شخص بعينه. نقاط القوة: مباشرة وسهلة الفهم، والإجابة عن «من يستطيع رؤية هذا الكائن؟» موجودة في القائمة نفسها. نقاط الضعف: عندما يكون لديك عشرة آلاف فاتورة فلديك عشرة آلاف قائمة، وعندما ينضم محاسب جديد عليك أن تضيفه إلى كل قائمة، وعندما يغادر عليك أن تتذكر حذفه منها كلها.
الصلاحيات حسب الدور RBAC، وانفجار الأدوار
في دليل IAM عرفنا 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. فقاعدة الاعتماد في مثالنا تكتب مرة واحدة: «يعتمد المحاسب الفاتورة إذا كانت من مؤسسته، وكانت مسودة، ومبلغها أقل من حده أو يساويه، ولم يكن هو من أنشأها»، ولا تحتاج إلى أي دور جديد عندما تنضم مؤسسة جديدة. وهذا ما سوف نكتبه حرفياً في التجربة العملية بلغة 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 في مؤتمر 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 الذي اعتمدته OASIS في يناير 2013. واللغة نفسها لم تعد شائعة في التطبيقات الحديثة، ولكن مصطلحاتها بقيت وتجدها في توثيق كل المحركات، لذلك يحسن أن تعرفها:
- نقطة تنفيذ السياسة Policy Enforcement Point واختصاراً PEP: الجزء من تطبيقك الذي يستقبل الطلب ويسأل عن القرار ثم ينفذه، أي يكمل الطلب أو يعيد 403. ويعرفها المعيار بأنها الجهة التي تؤدي التحكم بالوصول بإرسال طلبات القرار وتنفيذ قرارات التفويض.
- نقطة قرار السياسة Policy Decision Point واختصاراً PDP: المحرك الذي يقيم السياسة ويعطي القرار، مثل OPA أو OpenFGA.
- نقطة إدارة السياسة Policy Administration Point واختصاراً PAP: المكان الذي تكتب فيه السياسات وتدار، وهو في الغالب اليوم مستودع Git مع مراجعة، أو واجهة إدارة.
- نقطة معلومات السياسة Policy Information Point واختصاراً PIP: مصدر الحقائق التي يحتاجها القرار، مثل قاعدة بيانات الفواتير ومجموعات المستخدم في مزود الهوية وبيانات الموارد البشرية.
والصورة التالية تبين هذه الأدوار في طلب واحد، عندما يحاول bob اعتماد الفاتورة 42:

في الصورة أعلاه لاحظ أن السياسة لا تقرأ من Git عند كل طلب، وإنما تحمل إلى المحرك مسبقاً، ففي OPA مثلاً تنشر السياسات والبيانات كحزمة Bundle يسحبها المحرك دورياً ويحفظها في الذاكرة، لذلك يكون القرار سريعاً. ولاحظ أيضاً أن المدخل الذي يرسله التطبيق صغير وثابت الشكل: من هو المستخدم، وما الفعل، وما المورد، وهذا الشكل نفسه هو ما وحدته مجموعة AuthZEN في مؤسسة OpenID في معيار Authorization API 1.0 الذي أصبح نهائياً في 11 يناير 2026، والهدف منه أن يتحدث أي PEP مع أي PDP بالواجهة نفسها، فتستطيع أن تغير المحرك دون أن تعيد كتابة التطبيقات.
مكتبة داخل التطبيق أم خدمة مركزية؟
وقد يتساءل البعض: إذا كان كل طلب يحتاج إلى سؤال المحرك، ألا يضيف ذلك زمناً لكل طلب؟ والإجابة نعم، ولذلك توجد ثلاث طرق لتشغيل نقطة القرار، ولكل منها ثمن:
- مكتبة داخل التطبيق Embedded Library: المحرك يعمل داخل عملية التطبيق نفسها، فلا يوجد اتصال شبكة والقرار يستغرق أجزاء من المللي ثانية. وهذا ما تفعله Casbin، وما يمكن أن تفعله مع OPA عبر واجهة Go أو بتحويل السياسة إلى WebAssembly كما في توثيق التكامل في OPA، ومع Cedar التي تأتي كمكتبة. وثمنها أن كل تطبيق يحتاج إلى نسخة محدثة من السياسة، وأن اللغة يجب أن تكون مدعومة.
- خدمة بجانب التطبيق Sidecar: نسخة من المحرك تعمل بجانب كل تطبيق على السيرفر نفسه أو في ال Pod نفسه، ويسألها التطبيق عبر localhost، وهي الطريقة الأكثر شيوعاً مع OPA كما يذكر توثيقها. والاتصال محلي وسريع، والسياسة تصلها من مكان مركزي كحزمة.
- خدمة مركزية Central Service: محرك واحد أو عنقود يسأله الجميع عبر الشبكة، وهذا هو الشكل الطبيعي لمحركات العلاقات مثل OpenFGA وSpiceDB، لأن العلاقات بيانات كبيرة تحفظ في قاعدة بيانات ولا يمكن نسخها إلى كل تطبيق. وثمنها اتصال عبر الشبكة لكل قرار، والحاجة إلى أن تكون الخدمة متاحة دائماً، فإذا توقفت توقف كل شيء، وهذا هو السلوك الصحيح لأن البديل هو أن يسمح التطبيق بكل شيء عندما لا يجد من يسأله.
والتخزين المؤقت Caching يقلل الزمن، ولكنه يأتي بمشكلة القرار القديم: إذا خزنت قرار «bob يستطيع رؤية الفاتورة» لخمس دقائق ثم سحبت منه الصلاحية، فهو يستطيع رؤيتها خمس دقائق أخرى. لذلك اجعل مدة التخزين قصيرة، واعرف ما يفعله المحرك نفسه، وسوف نعود إلى هذه النقطة في قسم الاتساق.
أين يكون الفحص: البوابة أم التطبيق؟
في دليل حماية أي تطبيق باستخدام authentik Forward Auth وضعنا أمام التطبيق بوابة تسأل authentik قبل أن يصل أي طلب، فهل يغني ذلك عن التفويض داخل التطبيق؟ والإجابة لا، والسبب كما تبين الصورة التالية أن البوابة لا ترى الفاتورة:

في الصورة أعلاه لاحظ التالي:
- الفحص العام Coarse-Grained عند البوابة: هل سجل المستخدم الدخول؟ هل هو في المجموعة المسموح لها بفتح هذا التطبيق أصلاً؟ هل المسار
/adminللمديرين فقط؟ وهذا مفيد جداً، لأنه يمنع الغرباء من الوصول إلى التطبيق قبل أن تصل طلباتهم إلى الكود، ولكنه لا يعرف شيئاً عن البيانات. - الفحص الدقيق Fine-Grained داخل التطبيق: التطبيق وحده يقرأ الفاتورة 42 من قاعدة البيانات ويعرف لمن هي وكم مبلغها، لذلك هو من يجب أن يسأل المحرك «هل يستطيع bob اعتماد هذه الفاتورة؟» عند كل طلب، وعلى السيرفر دائماً.
- القوائم تحتاج إلى معاملة خاصة: صفحة «فواتيري» لا تسأل عن فاتورة واحدة، وإنما عن كل ما يستطيع المستخدم رؤيته، فإذا جلبت ألف فاتورة ثم سألت المحرك عن كل واحدة فالصفحة بطيئة. ومحركات العلاقات تجيب عن هذا السؤال مباشرة بأوامر مثل
list-objectsكما سوف نرى في التجربة، وأما مع محركات السياسات فالحل الشائع أن تضع شرط المؤسسة في استعلام قاعدة البيانات نفسه، ثم تسأل المحرك عن العمليات الحساسة.
أين تقع ال Scopes والحقول داخل التوكن؟
هناك خلط شائع بين ثلاثة أشياء تبدو متشابهة، وكلها تظهر في التوكن الذي يصل إلى تطبيقك:
- النطاقات Scopes في OAuth: كما شرحنا في دليل OAuth وOpenID Connect، ال Scope يحدد ما وافق المستخدم على أن يفعله تطبيق آخر نيابة عنه، مثل
invoices:read، فهو تفويض للتطبيق العميل Delegated Access وليس صلاحية المستخدم نفسه على فاتورة بعينها. فإذا أعطيت تطبيق التقارير Scope للقراءة، فهذا يعني أن التطبيق يستطيع أن يقرأ ما يستطيع المستخدم قراءته، ولا يعني أن المستخدم يستطيع قراءة كل الفواتير. لذلك ال Scope فحص عام يضاف إلى فحص التفويض ولا يحل محله. - المجموعات والأدوار من مزود الهوية: مزود الهوية يستطيع أن يضع في التوكن حقلاً مثل
groups: ["finance"]، وهو مصدر ممتاز للأدوار العامة لأن المجموعات تدار مركزياً، وقد شرحنا في دليل SCIM كيف تصل المجموعات إلى التطبيقات تلقائياً، وعند المغادرة تسحب. وهذه المجموعات تصبح أحد مدخلات القرار، أي خصائص يقرأها المحرك. - الصلاحية الدقيقة على مورد بعينه: «bob يعتمد الفاتورة 42» لا مكان لها في التوكن، لأنها تعتمد على الفاتورة وعلى حالتها الآن.
وقد يتساءل البعض: لماذا لا نضع كل صلاحيات المستخدم في JWT، فلا نحتاج إلى سؤال أي محرك؟ والإجابة أن لهذا الحل مشكلتين لا يمكن تجاوزهما. الأولى هي الحجم، فالتوكن يرسل مع كل طلب، وغالباً داخل ملف تعريف ارتباط له حد للحجم كما في دليل الجلسات وJWT، وقائمة بآلاف الفواتير لا تدخل فيه. والثانية هي القدم Staleness، فالتوكن يصدر مرة ويبقى صالحاً حتى ينتهي، وكما شرحنا في مشكلة إلغاء JWT، إذا سحبت من bob صلاحية بعد إصدار التوكن فالتوكن ما زال يقول إنه يملكها. لذلك اجعل التوكن يحمل الهوية والمجموعات العامة فقط، ودع المحرك يقرر الصلاحية الدقيقة عند كل طلب.
المحركات مفتوحة المصدر: ماذا تختار؟
فيما يلي المحركات التي سوف تقابلها، وقد راجعنا الترخيص والحالة لكل منها من مستودعها الرسمي في أكتوبر 2026، لأن بعضها تغير مالكه أو حالته خلال السنتين الماضيتين.
Open Policy Agent واختصاراً OPA
OPA محرك سياسات عام مكتوب بلغة Go بترخيص Apache-2.0، والسياسات فيه تكتب بلغة Rego، وهو لا يعرف شيئاً عن الفواتير أو Kubernetes، وإنما يأخذ مدخلاً بصيغة JSON وبيانات وسياسة ويعيد قراراً بصيغة JSON، لذلك يستخدم في التطبيقات وفي Kubernetes وفي فحص ملفات Terraform وغيرها. وقد قبل في CNCF عام 2018، ووصل إلى مستوى Graduated في 29 يناير 2021 كما في صفحته في CNCF. نقاط القوة: عام لكل شيء، وناضج جداً، وفيه اختبارات للسياسات وسجل للقرارات وتوزيع للسياسات كحزم، ويعمل كخدمة أو Sidecar أو مكتبة Go أو WebAssembly. نقاط الضعف: لغة Rego تحتاج إلى وقت للتعود عليها، وهو ليس مصمماً لحفظ ملايين العلاقات، فالبيانات التي يحتاجها يجب أن تصله مع المدخل أو كحزمة.
Cedar
Cedar لغة سياسات ومحرك مكتوب بلغة Rust، نشرته AWS كمصدر مفتوح بترخيص Apache-2.0 في مايو 2023، وهو المحرك الذي تقوم عليه خدمة Amazon Verified Permissions، وقد قبل في CNCF بمستوى Sandbox في 8 أكتوبر 2025 كما في صفحته في CNCF. والسياسات فيه تكتب بصيغة permit وforbid قريبة من اللغة الطبيعية، وقاعدة forbid تغلب permit دائماً، واللغة مصممة لتكون قابلة للتحليل الآلي، فتستطيع أن تتحقق بأدوات رياضية من أن تعديلاً ما لا يوسع الصلاحيات. نقاط القوة: لغة سهلة القراءة، وتعبر عن RBAC وABAC وجزء من العلاقات، وسريعة لأنها مكتبة. نقاط الضعف: تأتي كمكتبة بالدرجة الأولى، فعليك أن تبني حولها توزيع السياسات والسجلات بنفسك، والمكتبة الأساسية بلغة Rust مع مكتبة رسمية cedar-go للغة Go.
OpenFGA
OpenFGA محرك علاقات مستوحى من Zanzibar، بدأه موظفون في Okta وهو أساس الخدمة التجارية Auth0 FGA، ونشر كمصدر مفتوح في يونيو 2022، ويكتب بلغة Go بترخيص Apache-2.0، وقد انتقل في CNCF إلى مستوى Incubating في 28 أكتوبر 2025 كما في صفحته في CNCF وإعلان المشروع. والنموذج فيه يكتب بلغة DSL بسيطة، والعلاقات تحفظ في PostgreSQL أو MySQL أو SQLite، ويدعم الشروط Conditions لإضافة خصائص إلى العلاقة، مثل علاقة صالحة حتى تاريخ معين. نقاط القوة: نموذج سهل القراءة، وأوامر جاهزة للسؤال العكسي مثل list-objects وlist-users، وأداة سطر أوامر تختبر النموذج. نقاط الضعف: عليك أن تبقي العلاقات متزامنة مع بيانات تطبيقك، وهو خدمة مركزية يجب أن تكون متاحة دائماً.
SpiceDB
SpiceDB من شركة AuthZed، بترخيص Apache-2.0 ومكتوب بلغة Go، ويصف نفسه في توثيقه بأنه محرك قريب جداً من البنية التي وصفتها ورقة Zanzibar، ويحفظ العلاقات في PostgreSQL أو MySQL أو CockroachDB أو Spanner أو في الذاكرة للتجارب. وأهم ما يميزه أنه يطبق حل Zanzibar لمشكلة «العدو الجديد» عبر رموز الاتساق ZedTokens، وسوف نشرح هذه المشكلة في قسم الاتساق. نقاط القوة: الأقرب إلى Zanzibar في الاتساق والأداء على الأحجام الكبيرة، ولغة Schema واضحة مع أدوات تحقق. نقاط الضعف: يحتاج إلى فهم جيد لنموذج الاتساق، وبعض الميزات المتقدمة في الخدمة التجارية.
Ory Keto
Ory Keto من شركة Ory بترخيص Apache-2.0 ومكتوب بلغة Go، ويصف نفسه في توثيقه بأنه أول تطبيق مفتوح المصدر لمبادئ Zanzibar، والنموذج فيه يكتب بلغة Ory Permission Language وهي جزء من TypeScript. نقاط القوة: يتكامل مع بقية أدوات Ory مثل Kratos وHydra إذا كنت تستخدمها. نقاط الضعف: مجتمعه وأدواته أصغر من OpenFGA وSpiceDB، ونشاط المستودع أقل منهما.
Casbin
Casbin مكتبة وليست خدمة، بترخيص Apache-2.0، ولها نسخ رسمية بلغات كثيرة منها Go وJava وNode.js وPHP وPython وC# وRust كما في توثيقها، والنموذج فيها يوصف في ملف إعداد بطريقة PERM (السياسة Policy والأثر Effect والأدوار Role والمطابقة Matchers)، فتستطيع أن تعبر به عن ACL وRBAC وABAC. وقد دخل المشروع حاضنة مؤسسة Apache في 7 فبراير 2026 كما في صفحة الحاضنة، لذلك انتقل المستودع إلى apache/casbin. نقاط القوة: لا يحتاج إلى أي خدمة إضافية، ويعمل داخل تطبيقك بلغتك. نقاط الضعف: كل تطبيق يحمل نسخته من القواعد، فإذا كان لديك عدة تطبيقات بلغات مختلفة عدت إلى مشكلة النسخ المتعددة، إلا إذا حفظت السياسات في قاعدة بيانات مشتركة عبر المحولات Adapters.
Cerbos
Cerbos نقطة قرار بترخيص Apache-2.0 ومكتوبة بلغة Go، والسياسات فيها ملفات YAML مع شروط بلغة CEL، وهي بلا حالة Stateless كما في توثيقها، أي أنها لا تحفظ بيانات المستخدمين ولا الموارد، وإنما يرسلها التطبيق مع كل طلب، وتعمل كخدمة أو Sidecar، وتدعم واجهة AuthZEN. وللشركة خدمة تجارية اسمها Cerbos Hub لإدارة السياسات وتوزيعها وجمع السجلات. نقاط القوة: السياسات بصيغة YAML يقرؤها من لا يعرف لغة برمجة، وفيها أدوات اختبار. نقاط الضعف: لا تحفظ العلاقات، فالمشاركة والمجلدات المتداخلة تحتاج إلى أن يجهز التطبيق بياناتها قبل السؤال.
Topaz
Topaz بترخيص Apache-2.0، ويجمع بين OPA كمحرك للسياسات ودليل مدمج مستوحى من Zanzibar للعلاقات، فتكتب السياسة بلغة Rego وتسأل فيها عن العلاقات. وقد أعلنت الشركة التي طورته Aserto في إعلانها الأخير إغلاق خدمتها السحابية بحلول 31 مايو 2025، مع استمرار المشروع المفتوح عبر مشرفيه. نقاط القوة: يجمع السياسات والعلاقات في محرك واحد. نقاط الضعف: لم تعد خلفه شركة، لذلك راجع نشاط المستودع ومن يدعمه قبل أن تبني عليه نظاماً جديداً.
Permify
Permify محرك علاقات مستوحى من Zanzibar ومكتوب بلغة Go، ويدعم RBAC وReBAC وABAC بلغة نمذجة خاصة به، وترخيصه AGPL-3.0، وهذا يختلف عن بقية القائمة، فإذا عدلت الكود وقدمته كخدمة للآخرين فعليك أن تنشر تعديلاتك، وقد شرحنا معنى ذلك في دليل التراخيص. وقد استحوذت عليه FusionAuth في نوفمبر 2025 مع التزام معلن بالاستمرار في دعم المصدر المفتوح. نقاط القوة: يجمع العلاقات والخصائص في نموذج واحد. نقاط الضعف: الترخيص يحتاج إلى مراجعة قانونية في بعض المؤسسات، ومستقبله مرتبط بخطة الشركة المالكة.
مقارنة سريعة
| المحرك | النموذج | اللغة | طريقة التشغيل | الترخيص | يناسب |
|---|---|---|---|---|---|
| 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 تكفيه دون خدمة جديدة تديرها.
وإذا كانت لديك عدة خدمات تحتاج إلى نقطة القرار نفسها، فقد شرحنا كيف تشغلها كمنصة مشتركة، وكيف تبقي علاقاتها متزامنة مع بيانات كل خدمة، في منصة صلاحيات مركزية لخدماتك.
تجربة عملية: سياسة في OPA وعلاقات في OpenFGA
في هذه التجربة نطبق مثال الفواتير مرتين: مرة كسياسة على الخصائص في OPA مع اختباراتها، ومرة كنموذج علاقات في OpenFGA. نفذنا الأوامر على جهاز فيه Docker Engine 29.8 كما في دليل تثبيت Docker، باستخدام ال 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:
mkdir -p ~/authz-lab/opa && cd ~/authz-lab/opapackage 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، وقد وضعناه في ملف للتبسيط:
{
"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 في قسم الأدوار: الدور
accountantشرط واحد فيها، ومعه المؤسسة والحالة والحد والملكية، وكلها خصائص. - جعلنا مفتاح البيانات
dbوليسinvoicesمباشرة، والسبب أن اسم الحزمةpackage invoicesيضع السياسة نفسها تحتdata.invoices، فلو وضعنا البيانات تحت الاسم نفسه لتعارضا، وقد ظهر لنا هذا الخطأ فعلاً في المحاولة الأولى بالرسالةrego_recursion_error: rule data.invoices.invoice is recursive. reasonsمجموعة من أسباب الرفض نعيدها مع القرار، فتظهر في رسالة الخطأ للمستخدم وفي سجل القرارات، وهي للشرح فقط، أما القرار نفسه فتحددهallow.
الخطوة 2: نختبر السياسة مثل أي كود
قبل أن نشغل أي سيرفر، نكتب اختبارات للحالات التي نهتم بها في ملف policy_test.rego، وكل دالة تبدأ ب test_ هي اختبار كما في توثيق الاختبارات في OPA:
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 مؤقت:
docker run --rm -v "$PWD:/policies" -w /policies openpolicyagent/opa:1.21.1 test . -vوالمخرج سوف يكون كما يلي:
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. وعند تشغيل الاختبارات نفسها على النسخة المعدلة:
policy_test.rego:27:
data.invoices_test.test_no_self_approval: FAIL (20.589296ms)
--------------------------------------------------------------------------------
PASS: 5/6
FAIL: 1/6والأمر يعود برمز الخروج 2، لذلك إذا شغلته في خط CI كما في دليل Gitea Actions فإن طلب الدمج الذي يحذف هذا السطر يفشل قبل أن يصل إلى أي سيرفر، وهذه هي الفائدة الحقيقية من كتابة الصلاحيات ككود.
وتستطيع أيضاً أن تقيم السياسة لمدخل واحد دون سيرفر بالأمر opa eval، وهو مفيد عند التحقيق في قرار غريب. نكتب المدخل في ملف input.json ثم نقيمه:
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'والمخرج سوف يكون كما يلي:
falseالخطوة 3: OPA كخدمة يسألها التطبيق
الآن نشغل OPA كسيرفر، ونحمل فيه السياسة والبيانات، ونفعل طباعة سجل القرارات في المخرج حتى نراه:
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 بطلب POST إلى مسار القاعدة التي يريدها، وهنا /v1/data/invoices/decision. نجرب خمسة طلبات من خمسة مستخدمين:
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والمخرج سوف يكون كما يلي:
> {"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 ترسل هذه السجلات إلى خدمة مركزية كما في توثيق سجل القرارات:
docker logs lab-authz-opa 2>&1 | grep '"decision_id"' | sed -n 3p | python3 -m json.tool{
"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، ثم نسأل السؤال نفسه:
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والمخرج سوف يكون كما يلي:
204
{"decision_id":"902baabd-3d4a-4140-bd84-a8a862a04e3d","result":{"allow":true,"reasons":[]}}القرار تغير دون أن يتغير سطر واحد في السياسة أو في التطبيق، لأن الحد خاصية في البيانات. وفي الإنتاج لا تحدث البيانات يدوياً بهذا الشكل، وإنما تصل مع الحزمة أو يرسلها التطبيق مع المدخل، والمهم أن تعرف أن هناك مصدرين للقرار: السياسة التي تتغير بطلب دمج ومراجعة، والبيانات التي تتغير مع عمل المؤسسة.
الخطوة 4: نموذج العلاقات في OpenFGA
الآن نعبر عن جزء المشاركة من المثال كعلاقات. نشغل OpenFGA بقاعدة بيانات في الذاكرة، وهي مناسبة للتجربة فقط لأن كل شيء يختفي عند إيقافه، وللإنتاج تستخدم PostgreSQL كما في دليل التشغيل الرسمي. ونضع المحرك وأداة سطر الأوامر في شبكة واحدة:
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:
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 ونحمل فيه النموذج:
fga store create --name invoices --model model.fgaوالمخرج سوف يكون كما يلي:
{"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"}}ونحفظ معرف المخزن في متغير حتى تستخدمه بقية الأوامر:
export FGA_STORE_ID=01M476Z8P21CJ9MJRY1AAH8P81الخطوة 5: نكتب العلاقات ونسأل المحرك
نكتب العلاقات في ملف tuples.yaml: alice عضو في فريق المالية، وفريق المالية يملك مجلد فواتير 2026، والفاتورة 42 داخل هذا المجلد، وcarol تقرأ الفاتورة 43 مباشرة:
- 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-43fga tuple write --file tuples.yamlوالمخرج سوف يكون كما يلي:
{"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، وهو ما يفعله التطبيق عند كل طلب:
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والمخرج سوف يكون كما يلي:
{"allowed":true,"resolution":""}
{"allowed":true,"resolution":""}
{"allowed":false,"resolution":""}
{"objects":["document:invoice-42"]}لاحظ أنه لا يوجد في كل ما كتبناه سطر يقول «alice تقرأ الفاتورة 42»، والمحرك استنتج ذلك بالمشي في الرسم البياني للعلاقات كما تبين الصورة التالية، ولاحظ أن alice محررة أيضاً، لأن فريقها يملك المجلد والنموذج يقول إن مالك المجلد يحرر ما بداخله، فإذا لم يكن هذا ما تريده فالخطأ في النموذج وليس في المحرك، وهذا سبب آخر لاختبار النموذج كما سوف نرى. وأما الأمر الأخير list-objects فهو السؤال العكسي الذي تحتاجه صفحة «فواتيري»: ما المستندات التي تستطيع alice قراءتها؟

والآن ينضم bob إلى فريق المالية. في نظام الشروط المتفرقة كنت ستبحث عن كل مكان يقرر من يرى الفواتير، أما هنا فهي علاقة واحدة:
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والمخرج سوف يكون كما يلي:
{"successful":[{"object":"team:finance","relation":"member","user":"user:bob"}]}
{"allowed":true,"resolution":""}
{"objects":["document:invoice-42"]}وعندما يغادر bob الفريق نحذف العلاقة، فيسحب منه الوصول في الطلب التالي مباشرة، وهذا ما تريده تماماً عندما يصل حدث المغادرة من SCIM:
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والمخرج سوف يكون كما يلي:
{}
{"allowed":false,"resolution":""}
{"users":[{"object":{"id":"alice","type":"user"}}]}والأمر الأخير list-users هو جواب سؤال مدير المالية في بداية المقال: «من يستطيع رؤية هذه الفاتورة؟»، والإجابة الآن alice وحدها، وهي إجابة يعطيها المحرك لا يستنتجها أحد من قراءة الكود.
والتطبيق في الإنتاج لا يستخدم أداة سطر الأوامر، وإنما يرسل الطلب نفسه إلى واجهة HTTP، وهنا تستطيع أن تحدد مستوى الاتساق لكل طلب، وسوف نشرح معناه بعد قليل:
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"}'{"allowed":true,"resolution":""}ماذا يحدث إذا كتبت علاقة لا يسمح بها النموذج؟
النموذج يقول إن مالك المجلد لا يكون إلا أعضاء فريق، فلنحاول أن نجعل bob مالكاً للمجلد مباشرة:
fga tuple write user:bob owner folder:invoices-2026Error: 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: نختبر النموذج أيضاً
النموذج كود مثل السياسة، ويختبر بملف يجمع النموذج وعلاقات تجريبية والنتائج المتوقعة، كما في توثيق اختبار النماذج. نكتب الملف store.fga.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-42fga model test --tests store.fga.yamlوالمخرج سوف يكون كما يلي:
# Test Summary #
Tests 1/1 passing
Checks 3/3 passing
ListObjects 1/1 passingهذا الاختبار لا يحتاج إلى السيرفر، فيعمل في خط CI مثل opa test، وأي تعديل في النموذج يوسع صلاحية دون قصد يكسر أحد التوقعات قبل النشر.
إزالة التجربة
نحذف فقط ما أنشأناه، بالاسم:
docker rm -f lab-authz-opa lab-authz-openfga
docker network rm lab-authz-net
rm -rf ~/authz-labdocker system prune أو docker volume prune لإزالة تجربة، لأنها تحذف كل ما هو متوقف أو غير مرتبط على الجهاز، ومنه Volumes لمشاريع أخرى قد تكون متوقفة مؤقتاً. احذف ما أنشأته باسمه أو ببادئته فقط.الصلاحيات ككود في البنية التحتية: Kubernetes
الأفكار نفسها تطبق على البنية التحتية، ومن يعمل مع Kubernetes سوف يقابل ثلاث طبقات:
RBAC في Kubernetes: واجهة Kubernetes نفسها تستخدم الصلاحيات حسب الدور عبر أربعة أنواع من الكائنات كما في التوثيق الرسمي: Role لصلاحيات داخل Namespace واحد وClusterRole لصلاحيات على مستوى العنقود، وRoleBinding وClusterRoleBinding لربطها بمستخدم أو مجموعة أو Service Account. والصلاحيات فيها جمعية Additive فقط، أي لا توجد قواعد رفض، فكل ما لم يمنح فهو مرفوض، وتستطيع أن تسأل عن قرار بعينه بالأمر kubectl auth can-i. وهذه الطبقة تجيب عن «من يستطيع إنشاء Pod في هذا ال Namespace؟»، ولكنها لا تستطيع أن تقول «ولكن لا تسمح بأي Pod يعمل بصلاحيات root».
OPA Gatekeeper: هنا يأتي التحكم بالقبول Admission Control، أي فحص محتوى الكائن قبل أن يحفظ. وGatekeeper مشروع تابع ل OPA يعمل ك Webhook للقبول، والسياسات فيه بلغة Rego داخل كائنات Kubernetes من نوع ConstraintTemplate تعرف القاعدة وConstraint تطبقها بمعاملات، فتستطيع أن تمنع مثلاً أي Docker Image لا يأتي من ال Registry الخاص بك، وفيه وظيفة تدقيق Audit تكشف الكائنات الموجودة أصلاً التي تخالف السياسة.
Kyverno: وKyverno يحل المشكلة نفسها دون لغة جديدة، فالسياسات كائنات Kubernetes بصيغة YAML مع تعبيرات CEL، ويستطيع أن يتحقق من الكائنات ويعدلها وينشئ كائنات جديدة ويتحقق من توقيع ال Docker Images، وقد وصل إلى مستوى Graduated في CNCF في 16 مارس 2026 كما في صفحته في CNCF. والاختيار بين الاثنين في الغالب سؤال لغة: إذا كان فريقك يكتب Rego أصلاً للتطبيقات فGatekeeper يوحد اللغة، وإذا كان يفضل YAML فKyverno أسهل عليه.
وأما صلاحيات السيرفرات نفسها، مثل sudo وأقل صلاحية ممكنة للمديرين وشهادات SSH قصيرة العمر، فقد شرحناها في دليل إدارة الهوية والوصول IAM والوصول المميز PAM.
كيف تصمم الصلاحيات في نظامك؟
بعد أن رأيت النماذج والمحركات، فيما يلي القواعد التي تجعل التصميم صحيحاً مهما كان المحرك الذي تختاره:
- ابدأ بأدوار قليلة، وأضف الخصائص والعلاقات عند الحاجة: ثلاثة أو أربعة أدوار واضحة تكفي أغلب التطبيقات في البداية، وعندما تجد نفسك تنشئ دوراً جديداً لكل مؤسسة أو لكل حد مبلغ فهذه علامة أنك تحتاج إلى خاصية، وعندما تحتاج إلى مشاركة مجلد مع فريق فهذه علامة أنك تحتاج إلى علاقة.
- ارفض افتراضياً: كل ما لم تسمح به قاعدة صريحة مرفوض، وإذا لم يرد المحرك أو حدث خطأ في الاتصال به فالجواب رفض، والسبب أن الخطأ في اتجاه الرفض يزعج مستخدماً، أما الخطأ في اتجاه السماح فيكشف بيانات.
- افحص على السيرفر في كل طلب، ولا تثق بالواجهة: إخفاء زر «اعتماد» في الواجهة تحسين لتجربة المستخدم وليس حماية، فأي شخص يستطيع أن يرسل الطلب مباشرة بأداة مثل curl، لذلك يجب أن يسأل السيرفر المحرك عند كل طلب، وتستطيع الواجهة أن تسأله أيضاً لتقرر ما تعرضه، ولكن القرار الذي يحمي هو قرار السيرفر.
- سجل القرارات، لا الطلبات فقط: سجل كل قرار بالمستخدم والفعل والمورد والنتيجة والسبب، كما رأيت في سجل OPA، وراقب الرفض المتكرر لأنه غالباً شخص يجرب أرقام الفواتير.
- اختبر الصلاحيات مثل أي كود: لكل قاعدة اختبار يسمح واختبار يرفض، والاختبارات تعمل في CI عند كل طلب دمج، وتغيير السياسة يمر بمراجعة شخص ثان مثل أي كود حساس.
- اجعل الهوية في التوكن والقرار في المحرك: التوكن يحمل من هو المستخدم ومجموعاته العامة، والمحرك يقرر الصلاحية على كل مورد.
والصورة التالية تبين هذه الدورة الكاملة للسياسة من كتابتها إلى مراجعة قراراتها:

الاتساق ومشكلة «العدو الجديد»
عندما يكون المحرك خدمة موزعة تحفظ العلاقات على أكثر من سيرفر، أو عندما يستخدم التخزين المؤقت، تظهر مشكلة دقيقة سمتها ورقة Zanzibar مشكلة «العدو الجديد» New Enemy Problem، ومثالها في الورقة: تحذف alice شخصاً اسمه bob من قائمة الوصول لمجلد، ثم تطلب من زميل أن يضع مستندات جديدة في المجلد، فإذا قيم فحص الصلاحية على نسخة قديمة من العلاقات لم تصلها عملية الحذف بعد، فإن bob يرى المستندات الجديدة التي أضيفت بعد حذفه بالذات. والسبب أن المحرك لم يحترم ترتيب الأحداث، وهو ما شرحناه بشكل عام في دليل الأنظمة الموزعة ونماذج الاتساق.
وحل Zanzibar رمز صغير اسمه Zookie: عندما يحفظ التطبيق تعديلاً على المحتوى يطلب من المحرك رمزاً فيه طابع زمني يضمن أن كل تغييرات الصلاحيات السابقة قبله، ويحفظه مع المحتوى، ثم يرسله في كل فحص لاحق لهذا المحتوى، فيقيم المحرك الفحص على بيانات لا تقل حداثة عن هذا الرمز. وفي SpiceDB اسم هذا الرمز ZedToken، وتختار مستوى الاتساق لكل طلب كما في توثيق الاتساق. وفي OpenFGA الخيار أبسط كما في توثيقه: MINIMIZE_LATENCY وهو الافتراضي ويخدم من الذاكرة المؤقتة إذا كانت مفعلة، وHIGHER_CONSISTENCY الذي يتجاوزها ويقرأ من قاعدة البيانات مباشرة، والتوثيق نفسه ينبه إلى أنك إذا كتبت علاقة ثم فحصت مباشرة بالخيار الأول فقد لا يظهر التغيير، لذلك استخدم الخيار الثاني بعد سحب صلاحية أو في العمليات الحساسة، والأول في الباقي.
الانتقال من الشروط المتفرقة إلى محرك
لا تحاول أن تنقل كل الصلاحيات في يوم واحد، لأن أي اختلاف بين القديم والجديد سوف يظهر كخطأ عند المستخدمين، والطريقة الآمنة كما يلي:
- اجرد الشروط الموجودة: ابحث في الكود عن كل
ifيقارن دوراً أو مالكاً أو مؤسسة، واكتبها كقواعد بلغة بشرية، وغالباً سوف تجد في هذه الخطوة تناقضات وثغرات قبل أن تكتب سطراً واحداً. - اجمعها خلف دالة واحدة: استبدل كل الشروط في التطبيق باستدعاء دالة واحدة مثل
authorize(user, action, resource)تحتوي المنطق القديم كما هو، فيصبح لديك مكان واحد تستبدله لاحقاً. - اكتب السياسة واختباراتها: انقل القواعد إلى المحرك، واكتب لكل قاعدة اختبار سماح واختبار رفض.
- شغل الوضع الظلي Shadow Mode: اجعل الدالة تسأل المحرك أيضاً، وتسجل كل حالة يختلف فيها قراره عن المنطق القديم، دون أن تستخدم قراره بعد. وبعد أسبوع أو اثنين من حركة حقيقية، كل اختلاف إما خطأ في السياسة الجديدة أو ثغرة في الكود القديم.
- بدل ثم احذف: عندما تختفي الاختلافات اجعل قرار المحرك هو المعتمد، ثم احذف المنطق القديم حتى لا يعود أحد لاستخدامه.
الأخطاء الشائعة
- الاعتماد على البوابة وحدها: 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 الوصول بعلاقة واحدة ويسحبه بحذفها، ويجيب عن «من يستطيع رؤية هذه الفاتورة؟» بأمر واحد.
دليلإدارة الهوية والوصول IAM والوصول المميز PAMمن يملك صلاحية root على سيرفراتك ومنذ متى، وكيف تضبط حسابات المديرين بصلاحيات مؤقتة وجلسات مسجلة.
دليلشرح OAuth وOpenID Connect للمبتدئينلماذا لا يجب أن يعرف أي تطبيق كلمة مرورك، وكيف يحل OAuth ذلك بتوكنات محدودة، وكيف يضيف OpenID Connect الدخول الموحد.
دليلكيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWTبروتوكول HTTP لا يتذكر أحداً، فكيف يبقى المستخدم مسجلاً بين الصفحات، ومتى تختار الجلسة على السيرفر ومتى تختار JWT.
دليلما هو SCIM؟ إدارة حسابات الموظفين تلقائياً في كل تطبيقاتكالدخول الموحد SSO يمنع الموظف المغادر من الدخول لكن حساباته تبقى نشطة، وSCIM ينشئ الحسابات ويعطلها في كل تطبيق تلقائياً.
دليلكيف تحمي أي تطبيق ويب بصفحة دخول authentik عبر Forward Authكيف تضع صفحة دخول authentik مع المصادقة الثنائية أمام أي أداة داخلية دون أن تعدل في التطبيق نفسه.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على OPA 1.21.1 وOpenFGA 1.21.0 مع fga CLI 0.8.1 على Docker Engine 29.8، ومراجعة تراخيص المحركات وحالتها من مستودعاتها الرسمية وصفحات CNCF.
- أكتوبر 2026: إضافة رابط إلى سلسلة المقالات التالية: منصة الصلاحيات المركزية، وOpenFGA عملياً، وOPA عملياً.