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

تحسين الصلاحيات من التهديدات وملاحظات المستخدمين

كيف تطوّر تصميم الصلاحيات بالتفكير في التهديدات والاستماع لملاحظات المستخدمين؟ ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تطبق نموذج STRIDE لتحديد تهديدات نظام الصلاحيات ومعالجاتها.
  • تقيّم ملاحظات المستخدمين بالحاجة الحقيقية وأقل صلاحية.
  • تصمم تفويضًا مؤقتًا بدورة حياة واضحة.
  • تنفذ سجل تدقيق للعمليات الحساسة بقواعده.

١نمذجة التهديدات: فكّر كمهاجم

بعد المراجعة نسأل سؤالًا استباقيًّا: ما الذي يمكن أن يسوء؟ نموذج STRIDE من Microsoft يذكّرك بست فئات من التهديدات، فتمر على كل عملية مهمة وتسأل عنها:

الفئةالسؤالفي «دورات»المعالجة
S انتحال الهويةهل يدّعي أحد أنه غيره؟سرقة رمز دخول موظفة التسجيلHTTPS، ومدة صلاحية قصيرة للرمز، ومصادقة ثنائية للموظفين
T العبث بالبياناتهل يغيّر أحد بيانات ليست له؟متدرب يغيّر حالة طلبه إلى approvedتفويض وتحقق من المدخلات
R الإنكارهل ينكر أحد ما فعله؟«لم أعتمد هذا الطلب!»سجل تدقيق: من، وماذا، ومتى
I كشف المعلوماتهل يرى أحد ما لا يحق له؟المدرب يرى جوالات المتدربينأقل صلاحية، وإخفاء الحقول
D تعطيل الخدمةهل يوقف أحد النظام؟آلاف الطلبات يوم فتح التسجيلتحديد معدل الطلبات
E رفع الصلاحيةهل يكسب أحد صلاحيات أعلى؟تعديل الدور عبر /api/meالحقول المسموحة فقط (الدرس السابق)

٢ملاحظات المستخدمين: طلب تغيير في الصلاحيات

من الخطة: «تطوير استراتيجيات لتحسين تصميم الصلاحيات بناءً على تجارب المستخدمين وملاحظاتهم». بعد شهر من التشغيل وصلت ملاحظات. كل ملاحظة تُقيَّم: ما الحاجة الحقيقية؟ وهل تحقيقها بأقل صلاحية ممكن؟

الملاحظةالحاجة الحقيقيةالقرار
المدربون: «نريد أرقام جوالات المتدربين»إبلاغ المتدربين بتغيير موعد المحاضرة✗ لا نكشف الأرقام. ✓ زر «إرسال تنبيه لمسجلي دورتي» يرسل النظام عبره الرسالة
نورة: «لا أستطيع العمل حين يكون أ. فهد في إجازة»اعتماد الاستردادات الكبيرة في غيابه✓ تفويض مؤقت بتاريخ انتهاء، يُسجَّل في سجل التدقيق
المحاسب: «أحتاج تقرير الإيرادات»التقرير المالي فقط✓ دور جديد «محاسب» بصلاحية report:finance فقط، لا دور مدير

٣التفويض المؤقت

بدل إعطاء نورة دور المدير «حتى يعود»، ثم نسيانه، نصمم تفويضًا مؤقتًا له دورة حياة واضحة:

diagram.mmd
stateDiagram-v2  [*] --> طلب_تفويض : المدير يفوّض نورة أسبوعًا  طلب_تفويض --> فعّال : موافقة + تاريخ انتهاء  فعّال --> منتهٍ : انتهاء المدة تلقائيًّا  فعّال --> ملغى : إلغاء مبكر  منتهٍ --> [*]  ملغى --> [*]

المدير يفوّض نورة أسبوعًا

موافقة + تاريخ انتهاء

انتهاء المدة تلقائيًّا

إلغاء مبكر

طلب_تفويض

فعّال

منتهٍ

ملغى

التفويض ينتهي تلقائيًّا بتاريخه، فلا يعتمد على تذكّر أحد لسحبه. وهذا يعالج «تراكم الصلاحيات» من الدرس السابق.

٤سجل التدقيق

فئة الإنكار في STRIDE لا يعالجها إلا سجل تدقيق Audit Log: لكل عملية حساسة سطر فيه من فعل، وماذا فعل، وعلى أي شيء، ومتى. قواعده:

القاعدةلماذا؟
يُكتب من الخادم لا من الواجهةلا يستطيع المستخدم تجاوزه
لا يُعدّل ولا يُحذفسجل يمكن تعديله لا يُثبت شيئًا
يقرؤه دور محددفيه معلومات حساسة عن عمل الموظفين
لا يحوي كلمات مرور ولا بيانات حساسة كاملةالسجل نفسه قد يتسرب

توقّع وتحقّق

«لم أعتمد هذا الطلب!» تهديد من فئة:

يعالجه سجل التدقيق.

المدربون يطلبون جوالات المتدربين لإبلاغهم. أفضل قرار:

ما الذي يجعل التفويض المؤقت آمنًا؟

من يكتب سطور سجل التدقيق؟

الخلاصة

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

متوسط

أضف سجل تدقيق لعملية اعتماد الطلبات: كل اعتماد يُسجَّل، والسجل يقرؤه من يملك audit:read فقط.

المطلوب:
  • كل اعتماد ناجح يضيف إلى audit سطرًا فيه: actor (اسم المستخدم)، و action = enrollment:approve، و target (رقم الطلب)، و at (الوقت).
  • GET /api/audit يتطلب audit:read (للمدير فقط).
  • محاولة اعتماد فاشلة (بلا صلاحية) لا تُسجَّل كاعتماد.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. PATCH /api/enrollments/2/approve 🔑
  2. PATCH /api/enrollments/1/approve 🔑
  3. GET /api/audit 🔑
  4. GET /api/audit 🔑
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • نورة تعتمد الطلب 2
  • سارة ممنوعة ← 403
  • السجل فيه اعتماد واحد فقط
  • السطر: نورة اعتمدت الطلب 2
  • وقت العملية at نص
  • نورة لا تقرأ السجل ← 403

💡 تلميح 1
اكتب دالة log(user, action, target) تضيف كائنًا إلى audit.
💡 تلميح 2
الوقت: new Date().toISOString().
💡 تلميح 3
استدعِ log بعد تغيير الحالة مباشرة، وأضف 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);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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