طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 241 · تحليل وتصميم برمجيات الويب
الدرس 21 · الوحدة 9: تقييم وتحسين تصميم صلاحيات المستخدمين

مبادئ متقدمة: الأذونات المشروطة والمصادقة متعددة العوامل

كيف تصمم صلاحيات تعتمد على الملكية والوقت والقيمة، وتضيف عاملًا ثانيًا للعمليات الحساسة؟ ⏱ نحو 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 تضيف عاملًا من نوع آخر: «شيء تملكه» (رمز يصل للجوال أو تطبيق مصادقة) أو «شيء أنت» (بصمة). من الخطة: «تطبيق مبادئ تصميم الصلاحيات المتقدمة مثل الأذونات المشروطة والمصادقة ذات العوامل المتعددة».

diagram.mmd
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
خدمة الرسائلالخادمalt[الرمز صحيح وخلال 5 دقائق][خاطئ أو منتهٍ]عملية حساسة: تشترط mfaأ. فهدالبريد + كلمة المرورأرسل رمزًا من 6 أرقام لجوالهمطلوب العامل الثانيالرمز 482913رمز دخول + mfa = true401 (وبعد 5 محاولات يُقفل)استرداد 900 ريال + رمز الدخول

لاحظ الربط بين الموضوعين: الاسترداد عملية حساسة، فسياستها تشترط أن يكون رمز الدخول ناتجًا عن مصادقة ثنائية (mfa = true). هذا شرط سياق آخر في دالة السياسة.

قرار تصميميفي «دورات»
من يُلزَم بـ MFA؟كل الموظفين والمدير؛ والمتدربون اختياريًّا
متى يُطلب مرة أخرى؟قبل العمليات الحساسة (الاسترداد، وتغيير الأدوار) إن مر أكثر من 15 دقيقة
العامل الثانيتطبيق مصادقة أفضل من الرسائل النصية، والرسائل بديل مقبول
الاسترجاعرموز احتياطية تُعطى عند التفعيل لاستخدامها إن فُقد الجوال

توقّع وتحقّق

«المدرب يسجل الحضور لدوراته أثناء انعقادها» تحتاج:

لماذا نجمع شروط القاعدة في دالة سياسة واحدة؟

رمز يصل إلى الجوال عامل من نوع:

الشرط الزمني يُحسب بوقت:

وقت الجهاز يغيّره المستخدم.

الخلاصة

تحدٍّ برمجي: سياسة الاسترداد

متقدم

المواصفة: «تسترد موظفة التسجيل المبالغ حتى 500 ريال، وما زاد يسترده المدير. ولا يسترد أحد مبلغ طلب اعتمده بنفسه». السياسة الحالية تسمح لكل من يملك صلاحية استرداد بأي مبلغ. أكمل canRefund.

المطلوب:
  • من اعتمد الطلب (approvedBy) لا يسترده أبدًا.
  • من يملك refund:any يسترد أي مبلغ.
  • من يملك refund:small يسترد 500 فما دون فقط.
  • غيرهم 403.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /api/enrollments/1/refund 🔑
  2. POST /api/enrollments/2/refund 🔑
  3. POST /api/enrollments/2/refund 🔑
  4. POST /api/enrollments/3/refund 🔑
  5. 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);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس