سجل تدقيق لا يستطيع التطبيق إعادة كتابته
سجل التدقيق موجود ليجيب عن سؤال واحد: من فعل ماذا ومتى؟ وقيمته كلها في أن هذه الإجابة لا تتغير.
المشكلة أن الحماية في معظم الأنظمة تعيش في الكود وحده، على شكل عرف داخلي: "لا نكتب استعلام حذف على هذا الجدول". عرف من هذا النوع يصمد إلى أول خطأ، أو أول مطور جديد لم يسمع به.
البديل الذي رفضته
فكرت أولا في حصر المنع داخل طبقة الوصول للبيانات: دالة واحدة تكتب في السجل، ولا توجد دالة تحذف. حل معقول على الورق، لكنه يفترض أن كل مسار في النظام يمر عبر تلك الطبقة، وأن أحدا لن يكتب استعلاما مباشرا في هجرة أو سكربت صيانة بعد سنة من الآن.
كل ضمانة من هذا النوع تقف على انضباط البشر، وهنا نقطة ضعفها.
ما فعلته
نقلت القيد إلى المحرك نفسه:
CREATE TRIGGER trg_audit_no_update BEFORE UPDATE ON admin_audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is append-only'); END;
CREATE TRIGGER trg_audit_no_delete BEFORE DELETE ON admin_audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is append-only'); END;
بعد هذين المشغّلين صار أي تعديل أو حذف على الجدول يفشل، أيا كان مصدره: التطبيق، هجرة، سكربت، أو طرفية إدارية.
لماذا يستحق العناء
لو اخترق أحد التطبيق ووصل إلى صلاحية الكتابة، يبقى عاجزا عن محو أثره، وسجل ما فعله باق في مكانه.
ولو أخطأت أنا نفسي وكتبت استعلام حذف في لحظة سهو، توقفني القاعدة قبل التنفيذ.
المصيدة التي وقعت فيها
طبقت النمط نفسه على جدول اعتماد العروض، فانكسرت اختباراتي. تبين أن بيانات الاختبار كانت تُنظف بحذف الصفوف، والحذف صار ممنوعا.
الحل ليس تعطيل المشغّل، بل إنشاء قاعدة جديدة لكل اختبار بدل تنظيف القديمة. وهي ممارسة أفضل على أي حال.
كيف تتأكد أنه يعمل
لا تصدق أن القيد موجود لمجرد أنك كتبته. اختبره:
assert.throws(() => db.exec("DELETE FROM admin_audit_log"), /append-only/);
عندي يعمل هذا الاختبار على ملفات الهجرة الحقيقية لا على مخطط مبسط، فالقيد الذي نجح في الاختبار هو نفسه القيد الذي يعيش في الإنتاج.