1. قبل البدء
تعتمد كلّ من Cloud Firestore وCloud Storage for Firebase وRealtime Database على ملفات الإعداد التي تكتبها لمنح إذن الوصول للقراءة والكتابة. يمكن أن يعمل هذا الإعداد، الذي يُطلق عليه اسم "قواعد الأمان"، أيضًا كنوع من المخطط لتطبيقك، وهو أحد أهم أجزاء تطوير تطبيقك. وسيرشدك هذا الدرس التطبيقي حول الترميز خلال هذه العملية.
المتطلبات الأساسية
- محرّر بسيط، مثل 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
بعد ذلك، انتقِل إلى دليل initial-state، حيث ستعمل لبقية هذا الدرس العملي:
$ cd codelab-rules/initial-state
الآن، ثبِّت التبعيات لتتمكّن من إجراء الاختبارات. إذا كان اتصال الإنترنت بطيئًا، قد يستغرق ذلك دقيقة أو دقيقتين:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
الحصول على Firebase CLI
إنّ Emulator Suite الذي ستستخدمه لتشغيل الاختبارات هو جزء من Firebase CLI (واجهة سطر الأوامر) التي يمكن تثبيتها على جهازك باستخدام الأمر التالي:
$ npm install -g firebase-tools
بعد ذلك، تأكَّد من تثبيت أحدث إصدار من واجهة سطر الأوامر. من المفترض أن يعمل هذا الدرس العملي مع الإصدار 8.4.0 أو الإصدارات الأحدث، ولكن تتضمّن الإصدارات الأحدث المزيد من إصلاحات الأخطاء.
$ firebase --version 9.10.2
3- إجراء الاختبارات
في هذا القسم، ستجري الاختبارات محليًا. وهذا يعني أنّه حان الوقت لتشغيل Emulator Suite.
بدء المحاكيات
يحتوي التطبيق الذي ستعمل عليه على ثلاث مجموعات رئيسية في 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- تعديل مسودّات منشورات المدوّنة
بعد ذلك، سيعدّل المؤلّفون مسودات المستندات أثناء تحسينهم لمسودات مشاركات المدونة. أنشئ قاعدة للشروط التي يمكن بموجبها تعديل مشاركة. أولاً، يمكن للمؤلف فقط تعديل مسوداته. يُرجى العِلم أنّه عليك هنا التحقّق من معرّف UID المكتوب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
. يجب أن تنتهي جميع الدوال بعبارة إرجاع، وستعرض دالتنا قيمة منطقية تشير إلى ما إذا كان أي من المتغيرين صحيحًا:
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. الخطوات التالية
تهانينا! لقد كتبت "قواعد الأمان" التي اجتازت جميع الاختبارات وحمَت التطبيق.
في ما يلي بعض المواضيع ذات الصلة التي يمكنك التعرّف عليها:
- مشاركة المدوّنة: كيفية مراجعة رمز "قواعد الأمان"
- درس تطبيقي حول الترميز: التعرّف على عملية التطوير باستخدام ميزة "التطوير بدون اتصال بالإنترنت" من خلال المحاكيات
- فيديو: كيفية إعداد التكامل المستمر للاختبارات المستندة إلى المحاكي باستخدام GitHub Actions