تخطَّ إلى المحتوى

قواعد الأمان في Firestore بالتفصيل

الدرس 6 من 9 · Firebase

في تطبيق عادي يقف خادمك بين المستخدم وقاعدة البيانات ويفحص كل طلب. في Firebase لا يوجد خادم تكتبه أنت: التطبيق يكلّم Firestore مباشرة. فمن يمنع مستخدماً من قراءة بيانات غيره، أو كتابة مليون إعجاب لمنشوره؟ الجواب: قواعد الأمان (Security Rules). رأيتها سريعاً في درسي Firestore والمصادقة، وفي هذا الدرس ستتقنها، لأنها الفرق بين تطبيق آمن وتطبيق مكشوف للجميع.

تخيّل القواعد مثل موظف الأمن على باب المبنى: يفحص هوية كل داخل وما يحمله، ويقرر: يمر أو يُرفض. ولا أحد يصل للغرف إلا من هذا الباب.

firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if true;
allow write: if request.auth != null;
}
}
}
  • match /posts/{postId}: القاعدة تنطبق على كل مستند في مجموعة posts، و {postId} متغير يحمل معرّف المستند.
  • allow عملية: if شرط: اسمح بالعملية إذا تحقق الشرط.
  • كل ما لم تسمح به صراحةً مرفوض. مجموعة لم تذكرها في القواعد لا يقرؤها ولا يكتبها أحد.

والعمليات تتفرع هكذا:

العملية تشمل
read get (مستند واحد) و list (استعلام)
write create و update و delete

داخل أي شرط عندك ثلاثة متغيرات:

المتغير ما يحمله
request.auth المستخدم صاحب الطلب (request.auth.uid معرّفه)، أو null للزائر
resource.data بيانات المستند الموجودة الآن في القاعدة
request.resource.data بيانات المستند كما ستصبح إذا قُبلت الكتابة

الفرق بين الأخيرين مهم: عند التعديل تقارن القديم (resource) بالجديد (request.resource).

أشهر نمط: كل مستخدم له مستند في users معرّفه هو uid نفسه، لا يقرؤه ولا يعدّله غيره:

firestore.rules
match /users/{uid} {
allow read: if request.auth != null && request.auth.uid == uid;
allow create: if request.auth != null && request.auth.uid == uid
&& request.resource.data.role == 'student';
allow update: if request.auth != null && request.auth.uid == uid
&& request.resource.data.role == resource.data.role;
}

المتغير {uid} من المسار نقارنه بمعرّف صاحب الطلب. جرّبنا القواعد على المحاكي:

المحاولة النتيجة
سارة تنشئ ملفها بدور student نجحت
سارة تقرأ ملفها نجحت
سارة تغيّر اسمها نجحت
سارة تغيّر دورها إلى admin permission-denied
عمر يقرأ ملف سارة permission-denied

لاحظ شرط التعديل: request.resource.data.role == resource.data.role يعني «الدور بعد التعديل يجب أن يساوي الدور قبله». تعدّل سارة أي حقل آخر، لكنها لا ترقّي نفسها.

الشرط request.auth != null && request.auth.uid == uid تكرر ثلاث مرات. القواعد تدعم الدوال:

firestore.rules
function signedIn() {
return request.auth != null;
}
function isOwner(uid) {
return signedIn() && request.auth.uid == uid;
}
match /users/{uid} {
allow read: if isOwner(uid);
allow create: if isOwner(uid) && request.resource.data.role == 'student';
allow update: if isOwner(uid) && request.resource.data.role == resource.data.role;
}

القواعد صارت تُقرأ كجملة: «اسمح بالقراءة إذا كان الطالب هو المالك».

التأكد من هوية الكاتب لا يكفي. يجب التأكد مما يكتبه أيضاً. هذه قاعدة إنشاء منشور:

firestore.rules
match /posts/{postId} {
allow read: if true;
allow create: if signedIn()
&& request.resource.data.authorId == request.auth.uid
&& request.resource.data.text is string
&& request.resource.data.text.size() > 0
&& request.resource.data.text.size() <= 200
&& request.resource.data.keys().hasOnly(['text', 'authorId', 'likes'])
&& request.resource.data.likes == 0;
}

كل سطر يسد ثغرة، وجرّبنا كل واحدة على المحاكي:

المحاولة الشرط الذي رفضها
زائر ينشئ منشوراً signedIn()
منشور باسم مؤلف آخر authorId == request.auth.uid
نص رقمي 123 text is string
نص فارغ size() > 0
نص من 201 حرف size() <= 200
حقل زائد pinned: true keys().hasOnly([...])
منشور يبدأ بألف إعجاب likes == 0

والمنشور السليم { text: 'أول منشور', authorId: uid, likes: 0 } قُبل. كل المحاولات المرفوضة أعادت الخطأ نفسه: permission-denied.

التعديل والحذف: المالك فقط

Section titled “التعديل والحذف: المالك فقط”
firestore.rules
allow update: if isOwner(resource.data.authorId)
&& request.resource.data.authorId == resource.data.authorId;
allow delete: if isOwner(resource.data.authorId) || isAdmin();
  • المالك يُعرف من resource.data.authorId: الحقل المخزن في المستند الحالي، لا ما يرسله الطلب.
  • الشرط الثاني يمنع تغيير authorId نفسه، فلا تُنقل الملكية لأحد.
المحاولة النتيجة
سارة تعدّل نص منشورها نجحت
سارة تنقل ملكية منشورها لعمر permission-denied
عمر يعدّل منشور سارة permission-denied
عمر يحذف منشور سارة permission-denied

الأدوار: قراءة مستند آخر بـ get

Section titled “الأدوار: قراءة مستند آخر بـ get”

المدير يجب أن يستطيع حذف أي منشور مخالف. دور المستخدم مخزن في مستنده في users، والدالة get تقرأ مستنداً آخر من داخل القواعد:

firestore.rules
function isAdmin() {
return signedIn()
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}

المسار يُكتب كاملاً، و $(...) تضع قيمة المتغير داخله. وهنا تتضح أهمية منع المستخدم من تغيير دوره: لو استطاعت سارة كتابة role: 'admin' في ملفها لانهار النظام كله. الدور يُمنح فقط من Console أو من كود موثوق على خادم.

القاعدة على مجموعة لا تنطبق تلقائياً على ما تحتها. ملاحظات المستخدم الخاصة في users/{uid}/notes تحتاج قاعدتها، وتُكتب متداخلة:

firestore.rules
match /users/{uid} {
allow read: if isOwner(uid);
match /notes/{noteId} {
allow read, write: if isOwner(uid);
}
}

المتغير uid من المسار الخارجي متاح في القاعدة الداخلية. سارة كتبت ملاحظتها بنجاح، ومحاولة عمر لقراءة ملاحظاتها رُفضت.

  • المحاكي: firebase emulators:start يشغّل Firestore محلياً بقواعدك، فتجرّب بلا خوف على بياناتك الحقيقية. وعند كل رفض يطبع المحاكي رقم السطر المسؤول في ملف القواعد.
  • Rules Playground في Console: تحاكي طلباً واحداً بهوية تختارها وترى هل يُقبل.
  • النشر: firebase deploy --only firestore:rules.
  1. اكتب قاعدة لمجموعة comments: القراءة للجميع، والإنشاء للمسجلين فقط بشرط أن يكون authorId هو معرّفهم.
  2. أضف تحققاً: النص من نوع string وطوله بين 1 و 300 حرف.
  3. اسمح بالحذف لصاحب التعليق فقط، وامنع التعديل تماماً.
  4. تحدٍّ: أضف حقل likes، واكتب قاعدة تعديل تسمح بتغيير likes وحده وبزيادة واحد فقط في كل مرة. (تلميح: قارن request.resource.data.likes بـ resource.data.likes + 1.)
  • allow read, write: if true: قواعد «وضع الاختبار» تفتح القاعدة للعالم كله. لا تنشر تطبيقاً بها أبداً.
  • الاكتفاء بـ request.auth != null: يكفي إنشاء حساب مجاني ليقرأ المهاجم بيانات كل المستخدمين. اربط الصلاحية بمالك المستند.
  • الثقة بـ authorId القادم من الطلب: في التعديل والحذف قارن بـ resource.data (المخزن)، لا بـ request.resource.data (المُرسَل).
  • نسيان المجموعات الفرعية: قاعدة الأب لا تغطيها، فتبقى مرفوضة حتى تكتب لها قاعدة.

سؤال

ما الفرق بين resource.data و request.resource.data؟

سؤال

مجموعة لم تُكتب لها أي قاعدة. من يستطيع قراءتها؟

  • القواعد هي الحماية الوحيدة لبياناتك، وكل ما لم يُسمح به مرفوض.
  • request.auth هوية الطالب، و resource.data المخزن، و request.resource.data القادم.
  • اربط كل مستند بمالكه، وتحقق من نوع البيانات وطولها وحقولها عند الكتابة.
  • الدوال تختصر الشروط، و get تقرأ مستنداً آخر لفحص الأدوار.
  • جرّب قواعدك على المحاكي قبل النشر، ولا تنشر بقواعد وضع الاختبار.