تحسين الصلاحيات من التهديدات وملاحظات المستخدمين
كيف تطوّر تصميم الصلاحيات بالتفكير في التهديدات والاستماع لملاحظات المستخدمين؟ ⏱ نحو 45 دقيقة.
- تطبق نموذج
STRIDEلتحديد تهديدات نظام الصلاحيات ومعالجاتها. - تقيّم ملاحظات المستخدمين بالحاجة الحقيقية وأقل صلاحية.
- تصمم تفويضًا مؤقتًا بدورة حياة واضحة.
- تنفذ سجل تدقيق للعمليات الحساسة بقواعده.
١نمذجة التهديدات: فكّر كمهاجم
بعد المراجعة نسأل سؤالًا استباقيًّا: ما الذي يمكن أن يسوء؟ نموذج STRIDE من Microsoft يذكّرك بست فئات من التهديدات، فتمر على كل عملية مهمة وتسأل عنها:
| الفئة | السؤال | في «دورات» | المعالجة |
|---|---|---|---|
| S انتحال الهوية | هل يدّعي أحد أنه غيره؟ | سرقة رمز دخول موظفة التسجيل | HTTPS، ومدة صلاحية قصيرة للرمز، ومصادقة ثنائية للموظفين |
| T العبث بالبيانات | هل يغيّر أحد بيانات ليست له؟ | متدرب يغيّر حالة طلبه إلى approved | تفويض وتحقق من المدخلات |
| R الإنكار | هل ينكر أحد ما فعله؟ | «لم أعتمد هذا الطلب!» | سجل تدقيق: من، وماذا، ومتى |
| I كشف المعلومات | هل يرى أحد ما لا يحق له؟ | المدرب يرى جوالات المتدربين | أقل صلاحية، وإخفاء الحقول |
| D تعطيل الخدمة | هل يوقف أحد النظام؟ | آلاف الطلبات يوم فتح التسجيل | تحديد معدل الطلبات |
| E رفع الصلاحية | هل يكسب أحد صلاحيات أعلى؟ | تعديل الدور عبر /api/me | الحقول المسموحة فقط (الدرس السابق) |
٢ملاحظات المستخدمين: طلب تغيير في الصلاحيات
من الخطة: «تطوير استراتيجيات لتحسين تصميم الصلاحيات بناءً على تجارب المستخدمين وملاحظاتهم». بعد شهر من التشغيل وصلت ملاحظات. كل ملاحظة تُقيَّم: ما الحاجة الحقيقية؟ وهل تحقيقها بأقل صلاحية ممكن؟
| الملاحظة | الحاجة الحقيقية | القرار |
|---|---|---|
| المدربون: «نريد أرقام جوالات المتدربين» | إبلاغ المتدربين بتغيير موعد المحاضرة | ✗ لا نكشف الأرقام. ✓ زر «إرسال تنبيه لمسجلي دورتي» يرسل النظام عبره الرسالة |
| نورة: «لا أستطيع العمل حين يكون أ. فهد في إجازة» | اعتماد الاستردادات الكبيرة في غيابه | ✓ تفويض مؤقت بتاريخ انتهاء، يُسجَّل في سجل التدقيق |
| المحاسب: «أحتاج تقرير الإيرادات» | التقرير المالي فقط | ✓ دور جديد «محاسب» بصلاحية report:finance فقط، لا دور مدير |
٣التفويض المؤقت
بدل إعطاء نورة دور المدير «حتى يعود»، ثم نسيانه، نصمم تفويضًا مؤقتًا له دورة حياة واضحة:
stateDiagram-v2 [*] --> طلب_تفويض : المدير يفوّض نورة أسبوعًا طلب_تفويض --> فعّال : موافقة + تاريخ انتهاء فعّال --> منتهٍ : انتهاء المدة تلقائيًّا فعّال --> ملغى : إلغاء مبكر منتهٍ --> [*] ملغى --> [*]
التفويض ينتهي تلقائيًّا بتاريخه، فلا يعتمد على تذكّر أحد لسحبه. وهذا يعالج «تراكم الصلاحيات» من الدرس السابق.
٤سجل التدقيق
فئة الإنكار في STRIDE لا يعالجها إلا سجل تدقيق Audit Log: لكل عملية حساسة سطر فيه من فعل، وماذا فعل، وعلى أي شيء، ومتى. قواعده:
| القاعدة | لماذا؟ |
|---|---|
| يُكتب من الخادم لا من الواجهة | لا يستطيع المستخدم تجاوزه |
| لا يُعدّل ولا يُحذف | سجل يمكن تعديله لا يُثبت شيئًا |
| يقرؤه دور محدد | فيه معلومات حساسة عن عمل الموظفين |
| لا يحوي كلمات مرور ولا بيانات حساسة كاملة | السجل نفسه قد يتسرب |
توقّع وتحقّق
«لم أعتمد هذا الطلب!» تهديد من فئة:
المدربون يطلبون جوالات المتدربين لإبلاغهم. أفضل قرار:
ما الذي يجعل التفويض المؤقت آمنًا؟
من يكتب سطور سجل التدقيق؟
الخلاصة
STRIDE: انتحال، وعبث، وإنكار، وكشف معلومات، وتعطيل، ورفع صلاحية.- كل طلب تغيير: ما الحاجة الحقيقية؟ وكيف نلبيها بأقل صلاحية؟
- التفويض المؤقت ينتهي تلقائيًّا ويُسجَّل.
- سجل التدقيق: من، وماذا، وعلى ماذا، ومتى؛ يكتبه الخادم ولا يُعدَّل.
تحدٍّ برمجي: سجل تدقيق للاعتماد
متوسطأضف سجل تدقيق لعملية اعتماد الطلبات: كل اعتماد يُسجَّل، والسجل يقرؤه من يملك audit:read فقط.
- كل اعتماد ناجح يضيف إلى
auditسطرًا فيه: actor (اسم المستخدم)، و action =enrollment:approve، و target (رقم الطلب)، و at (الوقت). GET /api/auditيتطلبaudit:read(للمدير فقط).- محاولة اعتماد فاشلة (بلا صلاحية) لا تُسجَّل كاعتماد.
👀 اعرض الاستجابات المطلوبة
PATCH /api/enrollments/2/approve🔑PATCH /api/enrollments/1/approve🔑GET /api/audit🔑GET /api/audit🔑
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- نورة تعتمد الطلب 2
- سارة ممنوعة ← 403
- السجل فيه اعتماد واحد فقط
- السطر: نورة اعتمدت الطلب 2
- وقت العملية at نص
- نورة لا تقرأ السجل ← 403
💡 تلميح 1
log(user, action, target) تضيف كائنًا إلى audit.💡 تلميح 2
new Date().toISOString().💡 تلميح 3
authorize('audit:read') لمسار السجل.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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', 'audit:read'],};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, status: 'pending' }, { id: 2, traineeId: 5, status: 'pending' },];const audit = []; function log(user, action, target) { audit.push({ actor: user.name, action, target, at: new Date().toISOString() });} 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'; log(req.user, 'enrollment:approve', e.id); res.json(e);}); app.get('/api/audit', authenticate, authorize('audit:read'), (req, res) => { res.json(audit);}); app.listen(3000);