قواعد الأمان في Firestore بالتفصيل
في تطبيق عادي يقف خادمك بين المستخدم وقاعدة البيانات ويفحص كل طلب. في Firebase لا يوجد خادم تكتبه أنت: التطبيق يكلّم Firestore مباشرة. فمن يمنع مستخدماً من قراءة بيانات غيره، أو كتابة مليون إعجاب لمنشوره؟ الجواب: قواعد الأمان (Security Rules). رأيتها سريعاً في درسي Firestore والمصادقة، وفي هذا الدرس ستتقنها، لأنها الفرق بين تطبيق آمن وتطبيق مكشوف للجميع.
تخيّل القواعد مثل موظف الأمن على باب المبنى: يفحص هوية كل داخل وما يحمله، ويقرر: يمر أو يُرفض. ولا أحد يصل للغرف إلا من هذا الباب.
تذكير بالبنية
Section titled “تذكير بالبنية”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 |
المتغيرات الأساسية
Section titled “المتغيرات الأساسية”داخل أي شرط عندك ثلاثة متغيرات:
| المتغير | ما يحمله |
|---|---|
request.auth |
المستخدم صاحب الطلب (request.auth.uid معرّفه)، أو null للزائر |
resource.data |
بيانات المستند الموجودة الآن في القاعدة |
request.resource.data |
بيانات المستند كما ستصبح إذا قُبلت الكتابة |
الفرق بين الأخيرين مهم: عند التعديل تقارن القديم (resource) بالجديد (request.resource).
بيانات خاصة بكل مستخدم
Section titled “بيانات خاصة بكل مستخدم”أشهر نمط: كل مستخدم له مستند في users معرّفه هو uid نفسه، لا يقرؤه ولا يعدّله غيره:
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 يعني «الدور بعد التعديل يجب أن يساوي الدور قبله». تعدّل سارة أي حقل آخر، لكنها لا ترقّي نفسها.
الدوال: لا تكرر نفسك
Section titled “الدوال: لا تكرر نفسك”الشرط request.auth != null && request.auth.uid == uid تكرر ثلاث مرات. القواعد تدعم الدوال:
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;}القواعد صارت تُقرأ كجملة: «اسمح بالقراءة إذا كان الطالب هو المالك».
التحقق من صحة البيانات
Section titled “التحقق من صحة البيانات”التأكد من هوية الكاتب لا يكفي. يجب التأكد مما يكتبه أيضاً. هذه قاعدة إنشاء منشور:
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 “التعديل والحذف: المالك فقط”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 تقرأ مستنداً آخر من داخل القواعد:
function isAdmin() { return signedIn() && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';}المسار يُكتب كاملاً، و $(...) تضع قيمة المتغير داخله. وهنا تتضح أهمية منع المستخدم من تغيير دوره: لو استطاعت سارة كتابة role: 'admin' في ملفها لانهار النظام كله. الدور يُمنح فقط من Console أو من كود موثوق على خادم.
المجموعات الفرعية
Section titled “المجموعات الفرعية”القاعدة على مجموعة لا تنطبق تلقائياً على ما تحتها. ملاحظات المستخدم الخاصة في users/{uid}/notes تحتاج قاعدتها، وتُكتب متداخلة:
match /users/{uid} { allow read: if isOwner(uid);
match /notes/{noteId} { allow read, write: if isOwner(uid); }}المتغير uid من المسار الخارجي متاح في القاعدة الداخلية. سارة كتبت ملاحظتها بنجاح، ومحاولة عمر لقراءة ملاحظاتها رُفضت.
اختبر قواعدك قبل النشر
Section titled “اختبر قواعدك قبل النشر”- المحاكي:
firebase emulators:startيشغّل Firestore محلياً بقواعدك، فتجرّب بلا خوف على بياناتك الحقيقية. وعند كل رفض يطبع المحاكي رقم السطر المسؤول في ملف القواعد. - Rules Playground في Console: تحاكي طلباً واحداً بهوية تختارها وترى هل يُقبل.
- النشر:
firebase deploy --only firestore:rules.
جرّب بنفسك
Section titled “جرّب بنفسك”- اكتب قاعدة لمجموعة
comments: القراءة للجميع، والإنشاء للمسجلين فقط بشرط أن يكونauthorIdهو معرّفهم. - أضف تحققاً: النص من نوع
stringوطوله بين 1 و 300 حرف. - اسمح بالحذف لصاحب التعليق فقط، وامنع التعديل تماماً.
- تحدٍّ: أضف حقل
likes، واكتب قاعدة تعديل تسمح بتغييرlikesوحده وبزيادة واحد فقط في كل مرة. (تلميح: قارنrequest.resource.data.likesبـresource.data.likes + 1.)
أخطاء شائعة
Section titled “أخطاء شائعة”allow read, write: if true: قواعد «وضع الاختبار» تفتح القاعدة للعالم كله. لا تنشر تطبيقاً بها أبداً.- الاكتفاء بـ
request.auth != null: يكفي إنشاء حساب مجاني ليقرأ المهاجم بيانات كل المستخدمين. اربط الصلاحية بمالك المستند. - الثقة بـ
authorIdالقادم من الطلب: في التعديل والحذف قارن بـresource.data(المخزن)، لا بـrequest.resource.data(المُرسَل). - نسيان المجموعات الفرعية: قاعدة الأب لا تغطيها، فتبقى مرفوضة حتى تكتب لها قاعدة.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الفرق بين resource.data و request.resource.data؟
resource هو المخزن الآن، و request.resource ما سيصبح عليه المستند إذا قُبل الطلب.
سؤال
مجموعة لم تُكتب لها أي قاعدة. من يستطيع قراءتها؟
الأصل في قواعد Firestore الرفض: ما لم يُسمح به صراحة فهو ممنوع.
الخلاصة
Section titled “الخلاصة”- القواعد هي الحماية الوحيدة لبياناتك، وكل ما لم يُسمح به مرفوض.
request.authهوية الطالب، وresource.dataالمخزن، وrequest.resource.dataالقادم.- اربط كل مستند بمالكه، وتحقق من نوع البيانات وطولها وحقولها عند الكتابة.
- الدوال تختصر الشروط، و
getتقرأ مستنداً آخر لفحص الأدوار. - جرّب قواعدك على المحاكي قبل النشر، ولا تنشر بقواعد وضع الاختبار.