التخويل ليس المصادقة
سقالة المصادقة في Laravel تجيب عن من أنت. أما من يجوز له فعل ماذا فتكتبه أنت، والاختبارات التي تثبته هي التي لا يكتبها أحد: التي تؤكد الرفض.
يجعلك Laravel مصادَقًا عليك في أمسية واحدة. تسجيل، ودخول، وإعادة تعيين كلمة مرور، ورموز، وعامل ثانٍ إن أردت - مثبَّتة ومختبَرة ومتعارَف عليها.
ثم يحتاج التطبيق إلى تقرير ما يجوز لكل شخص مصادَق عليه، ويرفض الإطار بحق أن يخمّن ذلك. وذلك الجزء لك، وفيه تعيش أعطال الأمان التي نجدها في التدقيقات فعلًا.
الفرق، بصراحة
المصادقة تثبت من يقدّم الطلب. وهي مشكلة محلولة ولا ينبغي أن تحلّها بنفسك.
التخويل يقرر هل يجوز لذلك الشخص تنفيذ هذا الإجراء على هذا الكائن. وهو منطق مجال. لا أحد يستطيع شحنه لك، لأنه عبارة عن عملك أنت.
وكل عطل وصول خطير تقريبًا وجدناه في تطبيق Laravel كان عطل تخويل على مسار عملت فيه المصادقة بلا أي خلل.
العطل، في أشيع صوره
public function show(Invoice $invoice)
{
return view('invoices.show', compact('invoice'));
}المسار خلف middleware اسمه auth. والمستخدم مسجّل دخوله. وقد حلّ route model
binding المعرّف إلى فاتورة.
ولم يسأل شيء هل هي فاتورته هو. غيّر الرقم في الرابط وستحصل على فاتورة شخص آخر. وعلى منصة متعددة المستأجرين هذا تسريب بيانات عبر المستأجرين، والمسار يبدو طبيعيًا تمامًا في المراجعة.
والإصلاح سطر واحد والانضباط أن تتذكره في كل مرة:
public function show(Invoice $invoice)
{
$this->authorize('view', $invoice);
return view('invoices.show', compact('invoice'));
}قاعدة واحدة، مكان واحد، كل مدخل
العطل الثاني هو التكرار. تُنفَّذ صلاحية في متحكم لمسارات الويب، ثم يُعاد تنفيذها في middleware للواجهة البرمجية، ثم يُعاد تنفيذها تقريبًا في شرط Blade يخفي الزر.
ثلاث نسخ من قاعدة تتباعد. وتصير إحداها خاطئة، وهي عادةً التي لا ينظر إليها أحد.
القاعدة تنتمي إلى policy، وكل مدخل يستدعيها:
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $user->team_id === $invoice->team_id;
}
}المتحكمات تستدعيها. وموارد الواجهة البرمجية تستدعيها. وBlade يسأل
@can('view', $invoice) ليقرر هل يرسم الزر - وإخفاء الزر عرضٌ لا فرضٌ أبدًا.
الزر المخفي يبقى مسارًا.
والمهام وأوامر الكونسول تحتاج هذا التفكير أيضًا. التصدير المصفوف الذي يبني تقريرًا "لمستخدم" دون إعادة فحص النطاق وسيلةٌ لغسل فحص تخويل خارج النظام.
الملكية تغلب الأدوار
فحوص الأدوار تجيب عن نوع هذا المستخدم. وتمرّ بسعادة بينما يلمس المستخدم بيانات غيره.
// تمرّ لأي مدير، بما في ذلك مدير مستأجر آخر
if ($user->hasRole('admin')) { ... }
// تطرح السؤال المهم
return $user->team_id === $invoice->team_id
&& $user->hasRole('admin');في نظام متعدد المستأجرين، طبّق قيد المستأجر عالميًا بدل تذكّره في كل استعلام - نطاق عام، أو ربط ذو نطاق، أو طبقة مستودع لا يمكن تخطّيها بالصدفة. والقاعدة أن النسيان ينبغي أن ينتج صفر صفوف لا صفوف مستأجر آخر.
اختبر الرفض لا السماح
هذه هي الخلاصة العملية.
it('يرفض فاتورة تخص فريقًا آخر', function () {
$invoice = Invoice::factory()->create(); // فريق آخر ما
$intruder = User::factory()->create();
$this->actingAs($intruder)
->get("/invoices/{$invoice->id}")
->assertForbidden();
});اختبارات المسار السعيد تُكتب افتراضيًا لأنها الميزة التي تُبنى. والحالة السلبية
هي التي تلتقط الانتكاسة، ومجموعة اختبارات لا تحوي assertForbidden مجموعةٌ لم
تفحص التخويل قط.
اكتب واحدًا لكل مورد، للمستخدم الخطأ، وللمستأجر الخطأ، وللطلب غير المصادَق عليه. إنها أمسية قصيرة وهي أعلى اختبار قيمةً تستطيع أداءه في تطبيق أعمال.
ما نبحث عنه في تدقيق
كل مسار بلا استدعاء تخويل. وكل دالة policy تعيد true بلا شرط. والحقول القابلة
للإسناد الجماعي التي تقرر صلاحيات - وجود role أو team_id في $fillable
تصعيدٌ ينتظر إرسال نموذج. وكل موضع لا يكون فيه استعلام مقيَّدًا بالمستأجر الحالي.
والواجهة البرمجية، دائمًا، لأن فيها أعاد أحدهم تنفيذ قواعد مسارات الويب على عجل.
لا شيء من ذلك غريب. إنها المجموعة الصغيرة نفسها من الإغفالات، وهي أرخص اكتشافًا بقائمة فحص منها ببلاغ.
يمر التدقيق على قائمة الفحص هذه ويدوّن ما لم يجده. تلك الملاحظات هي الاختبارات التي لم يكتبها أحد. وإن كان الشك متعلقًا بالواجهة البرمجية تحديدًا، فابدأ من مستوى أعلى: Sanctum وPassport يرسمان حدّ الصلاحية في موضعين مختلفين، وأيّهما تستخدم يقرر كم من هذا ستبنيه بنفسك.
