1- قبل البدء
تعتمد Cloud Firestore وCloud Storage for Firebase وقاعدة البيانات في الوقت الفعلي على ملفات الإعداد التي تكتبها لمنح إمكانية الوصول للقراءة والكتابة. يمكن أن تعمل هذه الإعدادات، المسماة "قواعد الأمان"، أيضًا كنوع من المخططات لتطبيقك. وهو أحد أهم أجزاء تطوير التطبيق. وسيشرح لك هذا الدرس التطبيقي حول الترميز كيفية استخدامها.
المتطلبات الأساسية
- أداة تحرير بسيطة مثل Visual Studio Code أو Atom أو Sublime Text
- Node.js 8.6.0 أو إصدار أحدث (لتثبيت Node.js، استخدم nvm؛ للتحقق من الإصدار، شغِّل
node --version
) - Java 7 أو إصدار أحدث (لتثبيت Java، يُرجى استخدام هذه التعليمات، أو شغِّل
java -version
للتحقّق من الإصدار)
الأنشطة
في هذا الدرس التطبيقي حول الترميز، ستؤمّن منصة مدونات بسيطة تم تصميمها استنادًا إلى Firestore. ستستخدم محاكي Firestore لإجراء اختبارات الوحدات للتأكّد من توافقه مع "قواعد الأمان" والتأكّد من أنّ هذه القواعد تسمح بالوصول المتوقّع ولا تسمح به.
وستتعرّف على كيفية:
- منح أذونات دقيقة
- فرض عمليات التحقق من صحة البيانات والأنواع
- تنفيذ التحكم في الوصول المستند إلى السمات
- منح إمكانية الوصول استنادًا إلى طريقة المصادقة
- إنشاء دوال مخصصة
- إنشاء قواعد أمان مستندة إلى الوقت
- تنفيذ قائمة الحظر وعمليات الحذف الأولية
- التعرّف على حالات إلغاء تطبيع البيانات لاستيفاء أنماط الوصول المتعددة
2- إعداد
هذا تطبيق تدوين. في ما يلي ملخّص عالي المستوى لوظائف التطبيق:
مسودات مشاركات المدونة:
- يمكن للمستخدمين إنشاء مسودة مشاركات مدونة، تظهر في مجموعة
drafts
. - ويمكن للمؤلف مواصلة تعديل مسودة إلى أن تصبح جاهزة للنشر.
- عندما تكون جاهزة للنشر، يتم تشغيل دالة Firebase تؤدي إلى إنشاء مستند جديد في مجموعة
published
. - يمكن للمؤلف أو مشرفي الموقع الإلكتروني حذف المسودات.
مشاركات المدونة المنشورة:
- لا يمكن للمستخدمين إنشاء المشاركات المنشورة، بل يمكن إجراء ذلك فقط باستخدام دالة.
- ويمكن فقط حذفها مبدئيًا، ما يؤدي إلى تعديل السمة
visible
إلى "خطأ".
التعليقات
- تسمح المشاركات المنشورة بالتعليقات، وهي عبارة عن مجموعة فرعية في كل مشاركة منشورة.
- للحدّ من إساءة الاستخدام، يجب أن يكون لدى المستخدمين عنوان بريد إلكتروني تم إثبات ملكيته وألّا تكون جهة الرفض من أجل إضافة تعليق.
- لا يمكن تعديل التعليقات إلا بعد ساعة من نشرها.
- ويمكن لمؤلف التعليق أو مؤلف المشاركة الأصلية أو المشرفين حذف التعليقات.
بالإضافة إلى قواعد الوصول، عليك إنشاء "قواعد أمان" تفرض الحقول المطلوبة وعمليات التحقّق من صحة البيانات.
وكل شيء سيحدث محليًا باستخدام "حزمة محاكاة Firebase".
الحصول على رمز المصدر
في هذا الدرس التطبيقي حول الترميز، ستبدأ باختبارات حول "قواعد الأمان"، ولكن ستشبه قواعد الأمان نفسها، لذا عليك أولاً استنساخ المصدر لإجراء الاختبارات:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
بعد ذلك، انتقِل إلى دليل الحالة الأولية، حيث ستعمل خلال باقي هذا الدرس التطبيقي:
$ cd codelab-rules/initial-state
الآن، قم بتثبيت التبعيات حتى تتمكن من تشغيل الاختبارات. وإذا كان اتصال الإنترنت بطيئًا، قد يستغرق ذلك دقيقة أو اثنتين:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
الحصول على واجهة سطر الأوامر في Firebase
تعتبر "مجموعة أدوات المحاكاة" التي ستستخدمها لإجراء الاختبارات جزءًا من واجهة سطر الأوامر في Firebase (واجهة سطر الأوامر) التي يمكن تثبيتها على جهازك باستخدام الأمر التالي:
$ npm install -g firebase-tools
بعد ذلك، تأكَّد من استخدام أحدث إصدار من واجهة سطر الأوامر. من المفترض أن يعمل هذا الدرس التطبيقي مع الإصدار 8.4.0 أو الإصدارات الأحدث، ولكنّ الإصدارات الأحدث تتضمّن المزيد من إصلاحات الأخطاء.
$ firebase --version 9.10.2
3- إجراء الاختبارات
في هذا القسم، يمكنك إجراء الاختبارات محليًا. وهذا يعني أن الوقت قد حان لبدء تشغيل مجموعة أدوات المحاكاة.
تشغيل المحاكيات
يحتوي التطبيق الذي ستعمل عليه على ثلاث مجموعات رئيسية في Firestore: drafts
تحتوي على مشاركات مدونات قيد التقدم، وتحتوي مجموعة published
على مشاركات المدونات التي تم نشرها، وcomments
هي مجموعة فرعية تتناول المشاركات المنشورة. يشمل المستودع الإعلاني اختبارات الوحدة لقواعد الأمان التي تحدّد سمات المستخدم والشروط الأخرى المطلوبة لكي يتمكّن المستخدم من إنشاء المستندات وقراءتها وتعديلها وحذفها في مجموعات drafts
وpublished
وcomments
. ستكتب قواعد الأمان لاجتياز هذه الاختبارات.
للبدء، يتم تأمين قاعدة البيانات: يتم رفض القراءة والكتابة في قاعدة البيانات بشكل عام، وتفشل جميع الاختبارات. عند كتابة قواعد الأمان، ستمر الاختبارات. للاطّلاع على الاختبارات، افتح functions/test.js
في المحرِّر.
في سطر الأوامر، ابدأ أدوات المحاكاة باستخدام emulators:exec
ونفِّذ الاختبارات التالية:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
انتقِل إلى أعلى النتيجة:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
حاليًا هناك 9 إخفاقات. أثناء إنشاء ملف القواعد، يمكنك قياس مستوى التقدّم من خلال مشاهدة المزيد من الاختبارات التي تم اجتيازها.
4. إنشاء مسودات لمشاركات المدوّنة
نظرًا لاختلاف إمكانية الوصول إلى مسودة مشاركات المدونة عن الوصول إلى مشاركات المدونة المنشورة، يخزِّن تطبيق التدوين هذا مسودات مشاركات المدونة في مجموعة منفصلة، /drafts
. يمكن فقط للمؤلف أو المشرف الوصول إلى المسودات، كما يتم إجراء عمليات تحقق من صحة الحقول المطلوبة وغير القابلة للتغيير.
عند فتح ملف firestore.rules
، سيظهر لك ملف قواعد تلقائي:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
تستخدم عبارة المطابقة match /{document=**}
البنية **
لتطبيقها بشكل متكرر على جميع المستندات في المجموعات الفرعية. ولأنّها في المستوى الأعلى، تنطبق الآن القاعدة العامة نفسها على جميع الطلبات، بغضّ النظر عن مقدّم الطلب أو البيانات التي يحاول قراءتها أو كتابتها.
ابدأ بإزالة عبارة المطابقة الأعمق واستبدالها بـ match /drafts/{draftID}
. (يمكن أن تكون تعليقات بنية المستندات مفيدة في القواعد، وسيتم تضمينها في هذا الدرس التطبيقي حول الترميز، وتكون اختيارية دائمًا.)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
ستتحكّم القاعدة الأولى التي ستكتبها للمسودات في الأشخاص الذين يمكنهم إنشاء المستندات. في هذا التطبيق، لا يمكن إنشاء المسودات إلا من قِبل الشخص المدرج كمؤلف. تأكَّد من أنّ المعرّف الفريد الخاص بالمستخدم الذي يقدّم الطلب هو المعرّف الفريد نفسه الوارد في المستند.
سيكون الشرط الأول للإنشاء هو:
request.resource.data.authorUID == request.auth.uid
بعد ذلك، لا يمكن إنشاء المستندات إلا إذا كانت تتضمّن الحقول الثلاثة المطلوبة، وهي authorUID
وcreatedAt
وtitle
. (لا يوفّر المستخدم الحقل createdAt
، ويعني ذلك أنّه يجب على التطبيق إضافته قبل محاولة إنشاء مستند). بما أنّك تحتاج فقط إلى التأكد من أنّه يتم إنشاء السمات، يمكنك التأكّد من أنّ request.resource
يتضمن كل هذه المفاتيح:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
الشرط الأخير لإنشاء مشاركة مدونة هو ألّا يزيد طول العنوان عن 50 حرفًا:
request.resource.data.title.size() < 50
وبما أنّ جميع هذه الشروط يجب أن تكون صحيحة، يجب ربط هذه الشروط مع عامل التشغيل AND المنطقي &&
. تصبح القاعدة الأولى:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
إعادة إجراء الاختبارات في الوحدة الطرفية والتأكد من نجاح الاختبار الأول
5- تعديل مسودّات مشاركات المدونة
بعد ذلك، عندما يحسّن المؤلفون مسودات مشاركات المدونات، فسيتم تعديل مسودّات المستندات. أنشئ قاعدة لشروط إمكانية تعديل المشاركة. أولاً، يمكن للمؤلف فقط تحديث مسوداته. يُرجى العِلم أنّه يمكنك هنا التحقّق من المعرّف الفريد الذي تمت كتابته،resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
الشرط الثاني للتحديث هو عدم تغيير السمتَين authorUID
وcreatedAt
:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
وأخيرًا، يجب ألا يزيد عدد أحرف العنوان عن 50 حرفًا:
request.resource.data.title.size() < 50;
بما أنّه يجب استيفاء جميع الشروط، يجب إجراء تسلسل لها مع &&
:
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
تصبح القواعد الكاملة:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر.
6- حذف المسودات وقراءتها: التحكم في الوصول المستند إلى السمات
ومثلما يمكن للمؤلفين إنشاء المسودات وتعديلها، يمكنهم أيضًا حذف المسودات.
resource.data.authorUID == request.auth.uid
بالإضافة إلى ذلك، يُسمح للمؤلفين الذين لديهم السمة isModerator
في الرمز المميّز للمصادقة بحذف المسودات:
request.auth.token.isModerator == true
بما أنّ أيًا من هذين الشرطين كاف للحذف، يمكنك إجراء تسلسل له مع عامل تشغيل OR منطقي، يكون ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
تنطبق نفس الشروط على القراءات، بحيث يمكن إضافة الإذن إلى القاعدة:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
في ما يلي القواعد الكاملة:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر بنجاح.
7- قراءة المشاركات المنشورة وإنشاؤها وحذفها: إلغاء الضبط لأنماط الوصول المختلفة
نظرًا لاختلاف أنماط الوصول للمشاركات المنشورة ومسودّات المشاركات، يغيّر هذا التطبيق ترتيب المشاركات إلى مجموعات منفصلة من draft
وpublished
. على سبيل المثال، يمكن لأي شخص قراءة المشاركات المنشورة ولكن لا يمكن حذفها نهائيًا، بينما يمكن حذف المسودات ولكن لا يمكن قراءتها إلا من خلال المؤلف والمشرفين. في هذا التطبيق، عندما يريد أحد المستخدمين نشر مسودة منشور المدوّنة، يتم تشغيل دالة ستنشئ المشاركة الجديدة المنشورة.
بعد ذلك، ستكتب القواعد للمشاركات المنشورة. أبسط قواعد الكتابة هي أنّه يمكن لأي شخص قراءة المشاركات المنشورة، ولا يمكن لأي شخص إنشاءها أو حذفها. أضِف القواعد التالية:
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
عند إضافة هذه القواعد إلى القواعد الحالية، يصبح ملف القواعد بالكامل:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
إعادة إجراء الاختبارات والتأكّد من اجتياز اختبار آخر بنجاح
8- تعديل المشاركات المنشورة: الدوال المخصّصة والمتغيّرات المحلية
شروط تعديل مشاركة منشورة هي:
- يمكن للمؤلف أو المشرف فقط تنفيذ ذلك
- يجب أن يحتوي على جميع الحقول المطلوبة.
نظرًا لأنك كتبت شروطًا بالفعل لكي تكون مؤلفًا أو مشرفًا، يمكنك نسخ الشروط ولصقها، ولكن مع مرور الوقت قد يصبح من الصعب قراءتها وصيانتها. بدلاً من ذلك، ستقوم بإنشاء دالة مخصصة تضم المنطق لكونك مؤلفًا أو مشرفًا. وبعد ذلك، تطلبها من شروط متعددة.
إنشاء دالة مخصصة
أعلى عبارة المطابقة للمسودات، أنشئ دالة جديدة تسمى isAuthorOrModerator
تُستخدم كوسيطات في مستند مشاركة (يمكن استخدام ذلك مع المسودات أو المشاركات المنشورة) وكائن مصادقة المستخدم:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
استخدام المتغيّرات المحلية
داخل الدالة، استخدِم الكلمة الرئيسية let
لضبط المتغيّرَين isAuthor
وisModerator
. يجب أن تنتهي جميع الدوال بعبارة إرجاع، وستعرض دوالنا قيمة منطقية تشير إلى ما إذا كان أي من المتغير true:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
استدعِ الدالة.
ستعدِّل الآن قاعدة المسودات لاستدعاء هذه الدالة، مع الحرص على تمرير resource.data
كوسيطة أولى:
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
يمكنك الآن كتابة شرط لتحديث المشاركات المنشورة التي تستخدم أيضًا الدالة الجديدة:
allow update: if isAuthorOrModerator(resource.data, request.auth);
إضافة عمليات تحقُّق
يجب عدم تغيير بعض حقول المشاركة المنشورة، وتحديدًا الحقول url
وauthorUID
وpublishedAt
غير القابلة للتغيير. ويجب أن يظل الحقلان الآخران، title
وcontent
وvisible
موجودين بعد إجراء تحديث. أضِف شروطًا لفرض هذه المتطلبات على المشاركات المنشورة:
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
إنشاء دالة مخصّصة بنفسك
وأخيرًا، أضف شرطًا بأن يكون العنوان أقل من 50 حرفًا. ولأنّ هذا المنطق مُعاد استخدامه، يمكنك إجراء ذلك عن طريق إنشاء دالة جديدة، titleIsUnder50Chars
. باستخدام الدالة الجديدة، يصبح شرط تحديث المشاركة المنشورة:
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
وملف القاعدة الكامل هو:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
إعادة إجراء الاختبارات في هذه المرحلة، يجب أن يكون لديك 5 اختبارات ناجحة و4 اختبارات فاشلة.
9- التعليقات: المجموعات الفرعية وأذونات موفِّر تسجيل الدخول
تسمح المشاركات المنشورة بالتعليقات، ويتم تخزين التعليقات في مجموعة فرعية من المشاركة المنشورة (/published/{postID}/comments/{commentID}
). لا تنطبق قواعد المجموعة تلقائيًا على المجموعات الفرعية. لنفترض أنك لا تريد تطبيق القواعد نفسها التي تنطبق على المستند الرئيسي للمشاركة المنشورة على التعليقات، ستقوم بصياغة إطارات مختلفة.
لكتابة قواعد الوصول إلى التعليقات، ابدأ بعبارة المطابقة كما يلي:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
قراءة التعليقات: لا يمكن إخفاء الهوية
بالنسبة إلى هذا التطبيق، يمكن فقط للمستخدمين الذين أنشأوا حسابًا دائمًا وليس حسابًا مجهولاً قراءة التعليقات. لفرض هذه القاعدة، ابحث عن السمة sign_in_provider
في كل عنصر auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر.
إنشاء التعليقات: التحقّق من قائمة الحظر
هناك ثلاثة شروط لإنشاء تعليق:
- يجب أن يمتلك المستخدم بريدًا إلكترونيًا تم التحقق منه
- يجب ألا يزيد عدد أحرف التعليق عن 500 حرف
- لا يمكن إدراجه في قائمة المستخدمين المحظورين، والتي يتم تخزينها في متجر Firestore في مجموعة
bannedUsers
. أخذ هذه الحالات واحدة تلو الأخرى:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
القاعدة الأخيرة لإنشاء التعليقات هي:
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
أصبح ملف القواعد بالكامل الآن:
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر.
10- تعديل التعليقات: القواعد المستندة إلى الوقت
يتمثّل منطق العمل الخاص بالتعليقات في أنّه يمكن لمؤلف التعليق تعديلها لمدة ساعة بعد إنشائها. لتنفيذ ذلك، استخدِم الطابع الزمني للسمة createdAt
.
أولاً، لإثبات أنّ المستخدم هو المؤلف:
request.auth.uid == resource.data.authorUID
بعد ذلك، أنّه تم إنشاء التعليق خلال الساعة الأخيرة:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
عند الجمع بين هذه العوامل والعامل المنطقي AND، تصبح قاعدة تعديل التعليقات هي:
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر.
11- حذف التعليقات: التحقق من ملكية أحد الوالدين
يمكن حذف التعليقات من قِبل مؤلف التعليق أو المشرف أو مؤلف مشاركة المدونة.
أولاً، بما أنّ الوظيفة المساعدة التي أضفتها سابقًا لعمليات التحقّق من حقل authorUID
يمكن أن تكون مضمّنة في مشاركة أو تعليق، يمكنك إعادة استخدام الدالة المساعدة للتحقّق ممّا إذا كان المستخدم هو المؤلف أو المشرف:
isAuthorOrModerator(resource.data, request.auth)
للتحقّق مما إذا كان المستخدم هو مؤلف مشاركة المدونة، استخدِم get
للبحث عن المشاركة في Firestore:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
بما أنّ أيًا من هذه الشروط كافٍ، استخدِم عامل تشغيل OR منطقي بينها:
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
أعِد إجراء الاختبارات وتأكَّد من اجتياز اختبار آخر.
وملف القواعد بالكامل هو:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12- الخطوات التالية
تهانينا لقد كتبت قواعد الأمان التي أدت إلى اجتياز جميع الاختبارات وتأمين التطبيق!
إليك بعض المواضيع ذات الصلة التي يمكن التعمّق فيها لاحقًا:
- مشاركة مدونة: كيفية مراجعة قواعد الأمان لمراجعة الرموز البرمجية
- درس تطبيقي حول الترميز: نبذة عن التطوير المحلي الأول باستخدام أدوات المحاكاة
- فيديو: كيفية استخدام إعداد CI في الاختبارات المستندة إلى المحاكي باستخدام مهام GitHub