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

تنفيذ الأدوار والصلاحيات في الخادم

كيف يتحول تصميم الصلاحيات إلى كود في الخادم يحمي كل مسار بالمصادقة والتفويض والملكية؟ ⏱ نحو 50 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تحوّل مصفوفة الصلاحيات إلى بنية بيانات في الكود.
  • تنفذ وسيطي المصادقة والتفويض وتضعهما على المسارات.
  • تنفذ شرط الملكية داخل المعالج.
  • تطبق المنع افتراضيًّا بحماية كل مسارات API.

١من التصميم إلى الكود

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

لا

نعم

لا

نعم

لا يتحقق

يتحقق

طلب

رمز صحيح؟

401

الدور يملك الصلاحية؟

403

شرط الملكية إن وجد

403 أو 404

المعالج: 200

مسار كل طلب: المصادقة، ثم التفويض بالدور، ثم شرط الملكية إن وُجد.

٢وسيطا المصادقة والتفويض

في Express ننفذ البوابتين وسيطين Middleware يوضعان قبل معالج المسار: authenticate يعرف المستخدم من رمزه، وauthorize(perm) يتحقق من الصلاحية. لاحظ أن ROLE_PERMS نسخة حرفية من مصفوفة الصلاحيات:

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 });} app.get('/api/reports/finance', authenticate, authorize('report:finance'), (req, res) => {  res.json({ month: '2026-10', revenue: 12500 });}); app.listen(3000);

ثلاثة طلبات للمسار نفسه: بلا رمز ← 401، وسارة (متدربة) ← 403، وأ. فهد (مدير) ← 200. سطر المسار يُقرأ كجملة: «للتقرير المالي: صادِق، ثم اشترط report:finance، ثم نفّذ».

٣شرط الملكية: «الخاصة به»

في المصفوفة كتبنا للمتدرب enrollment:read:own: يرى طلباته فقط. هذا لا يكفيه وسيط بسيط؛ القرار يعتمد على البيانات نفسها، فيُكتب داخل المعالج:

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 enrollments = [  { id: 1, traineeId: 1, course: 'تصميم الويب', status: 'pending' },  { id: 2, traineeId: 5, course: 'قواعد البيانات', status: 'pending' },  { id: 3, traineeId: 6, course: 'تصميم الويب', status: 'approved' },]; app.get('/api/enrollments', authenticate, (req, res) => {  if (can(req.user, 'enrollment:read')) return res.json(enrollments);         // كل الطلبات  if (can(req.user, 'enrollment:read:own'))                                   // الخاصة به فقط    return res.json(enrollments.filter(e => e.traineeId === req.user.id));  res.status(403).json({ error: 'لا تملك الصلاحية' });                         // المنع افتراضيًّا}); app.listen(3000);

نورة (تسجيل) ترى الطلبات الثلاثة، وسارة ترى طلبها فقط، وخالد (مدرب) لا يملك أيًّا من الصلاحيتين فيصل إلى السطر الأخير: 403. هذا السطر تطبيق «المنع افتراضيًّا»: إن لم ينطبق أي شرط فالجواب لا.

٤المنع افتراضيًّا على مستوى التطبيق

أخطر ثغرة أن ينسى مبرمج وضع authenticate على مسار جديد. الحل: اجعل المصادقة إجبارية لكل مسارات API، واستثنِ العامة صراحة:

// المسارات العامة أولًا (قليلة ومعروفة)app.get('/api/courses', listPublicCourses); // كل ما بعد هذا السطر يحتاج دخولًاapp.use('/api', authenticate); app.get('/api/enrollments', /* … */);

هكذا يصبح النسيان آمنًا: المسار الجديد محمي تلقائيًّا، ومن يريد جعله عامًّا يجب أن يقرر ذلك صراحة.

توقّع وتحقّق

ما ترتيب الوسيطين في المسار المحمي؟

لا نعرف صلاحية مستخدم قبل أن نعرفه.

أين يُنفذ شرط «طلبات المتدرب الخاصة به فقط»؟

قرر العميل أن المحاسب يرى التقرير المالي. ماذا نعدل؟

ما فائدة app.use('/api', authenticate) ؟

الخلاصة

تحدٍّ برمجي: نفّذ مصفوفة الصلاحيات

متوسط

خادم «دورات» يصادق المستخدمين لكنه لا يتحقق من صلاحياتهم: كل من دخل يرى كل الطلبات ويعتمدها ويرى التقرير المالي. طبّق مصفوفة الصلاحيات.

المطلوب:
  • الاعتماد يتطلب enrollment:approve، والتقرير المالي يتطلب report:finance.
  • قائمة الطلبات: من يملك enrollment:read يرى الكل، ومن يملك enrollment:read:own يرى طلباته فقط، وغيرهما 403.
  • بلا رمز ← 401 كما هو.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /api/enrollments 🔑
  2. GET /api/enrollments 🔑
  3. GET /api/enrollments 🔑
  4. PATCH /api/enrollments/1/approve 🔑
  5. PATCH /api/enrollments/1/approve 🔑
  6. GET /api/reports/finance 🔑
  7. GET /api/reports/finance 🔑
  8. GET /api/enrollments
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • سارة (متدربة) ترى طلبها فقط
  • نورة (تسجيل) ترى الطلبات الثلاثة
  • خالد (مدرب) ← 403 على القائمة
  • سارة لا تعتمد ← 403
  • نورة تعتمد ← approved
  • نورة لا ترى التقرير المالي ← 403
  • أ. فهد يرى التقرير ← 200
  • بلا رمز ← 401
  • بلا مقارنة مباشرة بأسماء الأدوار (role ===)

💡 تلميح 1
أضف authorize('enrollment:approve') بعد authenticate في سطر مسار الاعتماد.
💡 تلميح 2
في قائمة الطلبات استخدم can(req.user, …) مرتين، ثم 403 في النهاية.
💡 تلميح 3
فلترة الملكية: enrollments.filter(e => e.traineeId === req.user.id).

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

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 enrollments = [  { id: 1, traineeId: 1, course: 'تصميم الويب', status: 'pending' },  { id: 2, traineeId: 5, course: 'قواعد البيانات', status: 'pending' },  { id: 3, traineeId: 6, course: 'تصميم الويب', status: 'approved' },]; app.get('/api/enrollments', authenticate, (req, res) => {  if (can(req.user, 'enrollment:read')) return res.json(enrollments);  if (can(req.user, 'enrollment:read:own'))    return res.json(enrollments.filter(e => e.traineeId === req.user.id));  res.status(403).json({ error: 'لا تملك الصلاحية' });}); app.patch('/api/enrollments/:id/approve', authenticate, authorize('enrollment:approve'), (req, res) => {  const e = enrollments.find(x => x.id === Number(req.params.id));  if (!e) return res.status(404).json({ error: 'غير موجود' });  e.status = 'approved';  res.json(e);}); app.get('/api/reports/finance', authenticate, authorize('report:finance'), (req, res) => {  res.json({ month: '2026-10', revenue: 12500 });}); app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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