مبادئ متقدمة: الأذونات المشروطة والمصادقة متعددة العوامل
كيف تصمم صلاحيات تعتمد على الملكية والوقت والقيمة، وتضيف عاملًا ثانيًا للعمليات الحساسة؟ ⏱ نحو 45 دقيقة.
- تميّز القواعد التي تحتاج شروطًا على المستخدم والمورد والسياق.
- تكتب سياسة مشروطة في دالة واحدة تطابق المواصفة.
- تشرح المصادقة متعددة العوامل وقرارات تصميمها.
- تجمع فصل المهام وشروط القيمة في سياسة واحدة.
١حين لا يكفي الدور
بعض القواعد لا يعبّر عنها الدور وحده. «المدرب يسجّل الحضور» صحيح، لكن: لأي دورة؟ ومتى؟ القاعدة الكاملة: «المدرب يسجّل الحضور لدوراته هو وأثناء انعقادها فقط». هذه أذونات مشروطة تجمع ثلاثة أنواع من الخصائص (فكرة ABAC):
| الخاصية | مثال |
|---|---|
| المستخدم | دوره، وهل هو صاحب السجل |
| المورد | الدورة: من مدربها؟ ما مبلغ الطلب؟ من اعتمده؟ |
| السياق | الوقت والتاريخ، ونوع الجهاز، وهل استخدم المصادقة الثنائية |
٢السياسة في دالة واحدة
نجمع القاعدة في دالة سياسة واحدة باسم واضح، بدل تفريق شروطها داخل المعالجات. هكذا تُقرأ وتُختبر وتُعدَّل في مكان واحد، وتطابق جملة المواصفات:
const express = require('express');const app = express();app.use(express.json()); // من مصفوفة الصلاحيات (الدرس 16) إلى الكود مباشرةconst ROLE_PERMS = { trainee: ['course:read', 'enrollment:create', 'enrollment:read:own'], trainer: ['course:read', 'attendance:record'], registrar: ['course:read', 'enrollment:read', 'enrollment:approve'], manager: ['course:read', 'enrollment:read', 'report:finance', 'user:manage'],};const users = [ { id: 1, name: 'سارة', role: 'trainee' }, { id: 2, name: 'نورة', role: 'registrar' }, { id: 3, name: 'أ. فهد', role: 'manager' }, { id: 4, name: 'خالد', role: 'trainer' },];// رموز دخول مبسطة للتجربة؛ في النظام الحقيقي JWT كما في مقرر أطر الويبconst tokens = { 'tok-sara': 1, 'tok-noura': 2, 'tok-fahad': 3, 'tok-khaled': 4 }; // 1) المصادقة: من أنت؟function authenticate(req, res, next) { const t = (req.headers.authorization || '').replace('Bearer ', ''); const user = users.find(u => u.id === tokens[t]); if (!user) return res.status(401).json({ error: 'سجّل الدخول أولًا' }); req.user = user; next();} // 2) التفويض: هل يحق لك؟const can = (user, perm) => (ROLE_PERMS[user.role] || []).includes(perm);function authorize(perm) { return (req, res, next) => can(req.user, perm) ? next() : res.status(403).json({ error: 'لا تملك الصلاحية ' + perm });} const TODAY = '2026-11-03'; // تاريخ اليوم ثابت في المثال حتى تتكرر النتيجةconst courses = [ { id: 1, title: 'تصميم الويب', trainerId: 4, start: '2026-11-01', end: '2026-11-12' }, { id: 2, title: 'قواعد البيانات', trainerId: 9, start: '2026-11-01', end: '2026-11-12' }, { id: 3, title: 'أمن الويب', trainerId: 4, start: '2026-12-01', end: '2026-12-10' },]; // السياسة: الدور + الملكية + الزمن، في مكان واحدfunction canRecordAttendance(user, course) { return can(user, 'attendance:record') && course.trainerId === user.id // دورته هو && TODAY >= course.start && TODAY <= course.end; // أثناء انعقادها} app.post('/api/courses/:id/attendance', authenticate, (req, res) => { const course = courses.find(c => c.id === Number(req.params.id)); if (!course) return res.status(404).json({ error: 'غير موجودة' }); if (!canRecordAttendance(req.user, course)) return res.status(403).json({ error: 'لا يمكنك تسجيل الحضور لهذه الدورة الآن' }); res.status(201).json({ course: course.title, recorded: req.body.present.length });}); app.listen(3000);
خالد مدرب في الطلبات الثلاثة الأولى: دورته الجارية ← 201، ودورة مدرب آخر ← 403، ودورته التي لم تبدأ ← 403. ونورة ليست مدربة أصلًا ← 403. كل شرط في الدالة سطر في جملة المتطلب.
٣المصادقة متعددة العوامل
كلمة المرور وحدها «شيء تعرفه» ويمكن أن يُسرق. المصادقة متعددة العوامل MFA تضيف عاملًا من نوع آخر: «شيء تملكه» (رمز يصل للجوال أو تطبيق مصادقة) أو «شيء أنت» (بصمة). من الخطة: «تطبيق مبادئ تصميم الصلاحيات المتقدمة مثل الأذونات المشروطة والمصادقة ذات العوامل المتعددة».
sequenceDiagram actor U as أ. فهد participant S as الخادم participant M as خدمة الرسائل U->>S: البريد + كلمة المرور S->>M: أرسل رمزًا من 6 أرقام لجواله S-->>U: مطلوب العامل الثاني U->>S: الرمز 482913 alt الرمز صحيح وخلال 5 دقائق S-->>U: رمز دخول + mfa = true else خاطئ أو منتهٍ S-->>U: 401 (وبعد 5 محاولات يُقفل) end U->>S: استرداد 900 ريال + رمز الدخول Note over S: عملية حساسة: تشترط mfa
لاحظ الربط بين الموضوعين: الاسترداد عملية حساسة، فسياستها تشترط أن يكون رمز الدخول ناتجًا عن مصادقة ثنائية (mfa = true). هذا شرط سياق آخر في دالة السياسة.
| قرار تصميمي | في «دورات» |
|---|---|
| من يُلزَم بـ MFA؟ | كل الموظفين والمدير؛ والمتدربون اختياريًّا |
| متى يُطلب مرة أخرى؟ | قبل العمليات الحساسة (الاسترداد، وتغيير الأدوار) إن مر أكثر من 15 دقيقة |
| العامل الثاني | تطبيق مصادقة أفضل من الرسائل النصية، والرسائل بديل مقبول |
| الاسترجاع | رموز احتياطية تُعطى عند التفعيل لاستخدامها إن فُقد الجوال |
توقّع وتحقّق
«المدرب يسجل الحضور لدوراته أثناء انعقادها» تحتاج:
لماذا نجمع شروط القاعدة في دالة سياسة واحدة؟
رمز يصل إلى الجوال عامل من نوع:
الشرط الزمني يُحسب بوقت:
الخلاصة
- الأذونات المشروطة: الدور + خصائص المورد + السياق (الوقت، و
MFA). - دالة السياسة الواحدة تُقرأ وتُختبر وتطابق جملة المواصفة.
MFA: شيء تعرفه + شيء تملكه أو شيء أنت؛ وتُطلب مجددًا قبل العمليات الحساسة.- الشروط الزمنية بوقت الخادم، والرموز بمدة صلاحية وحد للمحاولات.
تحدٍّ برمجي: سياسة الاسترداد
متقدمالمواصفة: «تسترد موظفة التسجيل المبالغ حتى 500 ريال، وما زاد يسترده المدير. ولا يسترد أحد مبلغ طلب اعتمده بنفسه». السياسة الحالية تسمح لكل من يملك صلاحية استرداد بأي مبلغ. أكمل canRefund.
- من اعتمد الطلب (
approvedBy) لا يسترده أبدًا. - من يملك
refund:anyيسترد أي مبلغ. - من يملك
refund:smallيسترد 500 فما دون فقط. - غيرهم 403.
👀 اعرض الاستجابات المطلوبة
POST /api/enrollments/1/refund🔑POST /api/enrollments/2/refund🔑POST /api/enrollments/2/refund🔑POST /api/enrollments/3/refund🔑POST /api/enrollments/3/refund🔑
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- نورة تسترد 300 ← تم
- نورة لا تسترد 900 (اعتمدته بنفسها، وفوق 500) ← 403
- المدير يسترد 900 ← تم
- المدير لا يسترد طلبًا اعتمده بنفسه ← 403
- المتدربة ← 403
- شرط المبلغ 500 في السياسة
💡 تلميح 1
if (e.approvedBy === user.id) return false;💡 تلميح 2
refund:any ← true.💡 تلميح 3
return can(user, 'refund:small') && e.amount <= 500;حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json()); // من مصفوفة الصلاحيات (الدرس 16) إلى الكود مباشرةconst ROLE_PERMS = { trainee: ['course:read', 'enrollment:create', 'enrollment:read:own'], trainer: ['course:read', 'attendance:record'], registrar: ['course:read', 'enrollment:read', 'enrollment:approve', 'refund:small'], manager: ['course:read', 'enrollment:read', 'report:finance', 'user:manage', 'refund:any'],};const users = [ { id: 1, name: 'سارة', role: 'trainee' }, { id: 2, name: 'نورة', role: 'registrar' }, { id: 3, name: 'أ. فهد', role: 'manager' }, { id: 4, name: 'خالد', role: 'trainer' },];// رموز دخول مبسطة للتجربة؛ في النظام الحقيقي JWT كما في مقرر أطر الويبconst tokens = { 'tok-sara': 1, 'tok-noura': 2, 'tok-fahad': 3, 'tok-khaled': 4 }; // 1) المصادقة: من أنت؟function authenticate(req, res, next) { const t = (req.headers.authorization || '').replace('Bearer ', ''); const user = users.find(u => u.id === tokens[t]); if (!user) return res.status(401).json({ error: 'سجّل الدخول أولًا' }); req.user = user; next();} // 2) التفويض: هل يحق لك؟const can = (user, perm) => (ROLE_PERMS[user.role] || []).includes(perm);function authorize(perm) { return (req, res, next) => can(req.user, perm) ? next() : res.status(403).json({ error: 'لا تملك الصلاحية ' + perm });} const enrollments = [ { id: 1, traineeId: 1, amount: 300, approvedBy: 3, refunded: false }, { id: 2, traineeId: 5, amount: 900, approvedBy: 2, refunded: false }, { id: 3, traineeId: 6, amount: 200, approvedBy: 3, refunded: false },]; function canRefund(user, e) { if (e.approvedBy === user.id) return false; // فصل المهام if (can(user, 'refund:any')) return true; return can(user, 'refund:small') && e.amount <= 500; // شرط المبلغ} app.post('/api/enrollments/:id/refund', authenticate, (req, res) => { const e = enrollments.find(x => x.id === Number(req.params.id)); if (!e) return res.status(404).json({ error: 'غير موجود' }); if (!canRefund(req.user, e)) return res.status(403).json({ error: 'غير مسموح' }); e.refunded = true; res.json(e);}); app.listen(3000);