المستوى المطلوب: متوسط. محتاج تكون مرتاح مع Express وفكرة الـ middleware، ومعاك طبقة مصادقة (JWT أو session) بتحدد المستخدم الحالي. لو لسه مبتدئ في Node، ابدأ بمقال بناء أول REST API بـ Express قبل ده.
هتخرج من المقال ده بنظام صلاحيات RBAC شغّال في Node.js، بيحدد مين يقدر يعمل إيه من مكان واحد، من غير ما تنثر شروط if في كل مسار. الفحص نفسه بياخد أقل من مللي ثانية، وإضافة دور جديد بتبقى تعديل سطر واحد بدل ما تلف على 40 ملف.
اعمل نظام صلاحيات (RBAC) في Node.js من الصفر
المشكلة باختصار
الطريقة الشائعة إنك تكتب if (user.isAdmin) { ... } جوّه كل route. دي بتفشل بسرعة. أول ما ييجي دور تالت اسمه "محرّر" بصلاحيات في النص، بتلاقي نفسك بتعدّل نفس الشرط في عشرات المسارات، وتنسى واحد، وتفتح ثغرة. ده بالظبط السبب إن "التحكم المكسور في الوصول" (Broken Access Control) طلع أخطر ثغرة في تصنيف OWASP لسنة 2021.
الحل مش شرط أذكى، الحل إنك تفصل القرار عن المكان. تحطّ "مين يقدر يعمل إيه" في جدول واحد، وتفرضه بنقطة فحص واحدة.
الفكرة ببساطة: بطاقات دخول المبنى
تخيّل مبنى شركة. مش كل موظف معاه مفتاح لكل باب. الاستقبال بياخد بطاقة "زائر" تفتح البوابة الرئيسية بس. الموظف العادي بطاقته تفتح مكتبه والمطبخ. المدير بطاقته تفتح كل حاجة. لاحظ إن الأبواب مش مربوطة بأسماء الناس، هي مربوطة بنوع البطاقة. لو عايز تغيّر صلاحيات كل الموظفين، بتعدّل إعداد البطاقة مرة واحدة، مش بتلف على كل موظف.
البطاقة هي الدور (Role)، والأبواب اللي بتفتحها هي الصلاحيات (Permissions). ده هو RBAC بالظبط.
علميًا، الـ RBAC اختصار Role-Based Access Control، وهو نموذج بيربط ثلاث طبقات بالترتيب: المستخدم ← الدور ← الصلاحية. المستخدم مبياخدش صلاحيات مباشرة، بياخد دور، والدور بيحمل مجموعة صلاحيات. الفصل ده بيخلّي أي تعديل في صلاحيات دور يسري فورًا على كل المستخدمين اللي عليه. النموذج موصّف رسميًا في معيار NIST/INCITS 359.
الخطوات
- عرّف مصفوفة الصلاحيات: جدول واحد يربط كل دور بالصلاحيات المسموح بيها.
- اكتب دالة فحص واحدة:
can(role, permission)ترجّع true/false. - غلّفها في middleware:
requirePermission()يقف قدام أي مسار. - اربط كل مسار بالصلاحية اللي يحتاجها.
1) مصفوفة الصلاحيات
سمّي الصلاحيات بصيغة مورد:فعل (زي post:delete) عشان تفضل مقروءة وقابلة للتوسّع. وحوّل كل مجموعة لـ Set عشان الفحص يبقى في زمن ثابت O(1) مهما كبر عدد الصلاحيات.
// permissions.js
const ROLES = {
admin: ['post:read', 'post:write', 'post:delete', 'user:manage'],
editor: ['post:read', 'post:write'],
viewer: ['post:read'],
};
// كل دور يتحوّل لـ Set عشان الفحص O(1) مش O(n)
const rolePerms = Object.fromEntries(
Object.entries(ROLES).map(([role, perms]) => [role, new Set(perms)])
);
function can(role, permission) {
return rolePerms[role]?.has(permission) ?? false;
}
module.exports = { can, ROLES };2) و 3) الوسيط requirePermission
الوسيط بياخد اسم الصلاحية المطلوبة ويرجّع دالة Express عادية. لو مفيش مستخدم يرجّع 401، ولو الدور مالوش الصلاحية يرجّع 403 ويقف الطلب فورًا قبل ما يوصل للمعالج.
// requirePermission.js
const { can } = require('./permissions');
function requirePermission(permission) {
return (req, res, next) => {
const role = req.user?.role; // جاي من طبقة المصادقة (JWT مثلاً)
if (!role) return res.status(401).json({ error: 'unauthenticated' });
if (!can(role, permission)) {
return res.status(403).json({ error: 'forbidden', needed: permission });
}
next();
};
}
module.exports = { requirePermission };4) اربط المسارات
دلوقتي كل مسار بيعلن الصلاحية اللي محتاجها في مكان واحد واضح. مفيش if جوّه المعالجات خالص.
const express = require('express');
const { requirePermission } = require('./requirePermission');
const app = express();
// افترض إن middleware المصادقة حطّ req.user = { id, role }
app.get ('/posts', requirePermission('post:read'), listPosts);
app.post ('/posts', requirePermission('post:write'), createPost);
app.delete('/posts/:id', requirePermission('post:delete'), deletePost);
app.get ('/admin/users', requirePermission('user:manage'), listUsers);التحقق من أنه يعمل
اختبر أسوأ حالة: مستخدم بدور viewer يحاول يحذف منشور. المفروض يرجّع 403، والمعالج deletePost ميتنفّذش أصلاً.
# viewer بيحاول يحذف
curl -i -X DELETE http://localhost:3000/posts/42 \
-H "Authorization: Bearer <viewer_token>"
# HTTP/1.1 403 Forbidden
# {"error":"forbidden","needed":"post:delete"}بعد كده جرّب نفس الطلب بـ admin token، المفروض يرجّع 200. لو الاتنين رجّعوا نفس الكود، يبقى الوسيط مش مركّب صح على المسار.
المقايضات: بتكسب إيه وبتخسر إيه
سيناريو واقعي: عندك لوحة تحكم فيها 40 مسار و3 أنواع مستخدمين. بالطريقة القديمة، إضافة دور "محرّر" معناها تفتح 40 ملف وتعدّل الشروط. بـ RBAC، نفس الإضافة بتبقى صف واحد في مصفوفة ROLES. ده الفرق بين تعديل في 40 مكان وتعديل في مكان واحد.
الـ trade-off هنا: بتكسب مركزية وتدقيق سهل (تقدر تجاوب على "مين يقدر يمسح؟" من جدول واحد)، وبتخسر المرونة على مستوى المورد الواحد. RBAC بيقولك "هل الدور ده يقدر يحذف منشورات؟" لكنه لوحده مبيعرفش يقول "هل يقدر يحذف هذا المنشور بالذات لأنه صاحبه؟". ده بيحتاج طبقة ملكية إضافية.
الافتراض اللي الشرح مبني عليه: عندك عدد أدوار محدود (أقل من ~20 دور) وصلاحيات على مستوى النوع مش على مستوى كل سجل. طول ما ده صح، RBAC بيفضل أبسط وأرخص من البدائل.
متى لا تستخدم هذه الطريقة
- لو محتاج صلاحيات على مستوى كل سجل (المستخدم يعدّل مقالاته هو بس): RBAC لوحده مش كفاية، ضيف فحص ملكية (
resource.ownerId === req.user.id) فوق فحص الصلاحية. - لو القواعد بتعتمد على سياق متغيّر (المكان، الوقت، قيمة الطلب): هنا نموذج ABAC (قائم على السمات) أنسب، لكنه أعقد في البناء والاختبار.
- لو التطبيق صغير بمستخدم واحد أو أدمن واحد: RBAC مبالغة هندسية، شرط
isAdminواحد بيكفي.
الخطوة التالية
افتح أكبر ملف routes في مشروعك، ودوّر على كلمة isAdmin أو role ===. كل نتيجة دي نقطة قرار متكررة. انقل أول واحدة بس لـ requirePermission() النهاردة، وشغّل اختبار الـ 403 فوق. لو نجح، عندك أساس تنقل عليه الباقي بالتدريج من غير ما توقف التطبيق.