تنفيذ الأدوار والصلاحيات في الخادم
كيف يتحول تصميم الصلاحيات إلى كود في الخادم يحمي كل مسار بالمصادقة والتفويض والملكية؟ ⏱ نحو 50 دقيقة.
- تحوّل مصفوفة الصلاحيات إلى بنية بيانات في الكود.
- تنفذ وسيطي المصادقة والتفويض وتضعهما على المسارات.
- تنفذ شرط الملكية داخل المعالج.
- تطبق المنع افتراضيًّا بحماية كل مسارات
API.
١من التصميم إلى الكود
صممنا في الوحدة السابقة الأدوار ومصفوفة الصلاحيات. الآن ننفذها في خادم الويب. الهدف أن يكون الكود مرآة للتصميم: إن قرأ العميل المصفوفة وقرأ المبرمج الكود رأيا الشيء نفسه. كل طلب يمر بثلاث بوابات بالترتيب:
٢وسيطا المصادقة والتفويض
في 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) ؟
الخلاصة
- كل طلب: مصادقة
(401)، ثم تفويض بالصلاحية(403)، ثم شرط الملكية. ROLE_PERMSنسخة من مصفوفة الصلاحيات، وcan(user, perm)تسأل عنها.- تحقق بالصلاحية لا باسم الدور، فيكفي تعديل المصفوفة عند تغير القرار.
- الملكية تعتمد على البيانات فتُفحص في المعالج.
app.use('/api', authenticate)يجعل النسيان آمنًا.
تحدٍّ برمجي: نفّذ مصفوفة الصلاحيات
متوسطخادم «دورات» يصادق المستخدمين لكنه لا يتحقق من صلاحياتهم: كل من دخل يرى كل الطلبات ويعتمدها ويرى التقرير المالي. طبّق مصفوفة الصلاحيات.
- الاعتماد يتطلب
enrollment:approve، والتقرير المالي يتطلبreport:finance. - قائمة الطلبات: من يملك
enrollment:readيرى الكل، ومن يملكenrollment:read:ownيرى طلباته فقط، وغيرهما 403. - بلا رمز ← 401 كما هو.
👀 اعرض الاستجابات المطلوبة
GET /api/enrollments🔑GET /api/enrollments🔑GET /api/enrollments🔑PATCH /api/enrollments/1/approve🔑PATCH /api/enrollments/1/approve🔑GET /api/reports/finance🔑GET /api/reports/finance🔑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);