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
إلى false.
تعليقات
- تسمح المنشورات المنشورة بالتعليقات، وهي عبارة عن مجموعة فرعية على كل مشاركة منشورة.
- للحد من إساءة الاستخدام، يجب أن يكون لدى المستخدمين عنوان بريد إلكتروني تم التحقق منه وألا يكونوا منفيين من أجل ترك تعليق.
- لا يمكن تحديث التعليقات إلا خلال ساعة من نشرها.
- يمكن حذف التعليقات بواسطة مؤلف التعليق، أو مؤلف المنشور الأصلي، أو بواسطة المشرفين.
بالإضافة إلى قواعد الوصول، ستقوم بإنشاء قواعد أمان تفرض الحقول المطلوبة وعمليات التحقق من صحة البيانات.
كل شيء سيحدث محليًا، باستخدام Firebase Emulator Suite.
احصل على الكود المصدري
في هذا الدرس التطبيقي حول التعليمات البرمجية، ستبدأ باختبارات لقواعد الأمان، ولكن مع محاكاة قواعد الأمان نفسها، لذا فإن أول شيء يتعين عليك فعله هو استنساخ المصدر لتشغيل الاختبارات:
$ 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 CLI
تعد Emulator Suite التي ستستخدمها لتشغيل الاختبارات جزءًا من Firebase CLI (واجهة سطر الأوامر) والتي يمكن تثبيتها على جهازك باستخدام الأمر التالي:
$ npm install -g firebase-tools
بعد ذلك، تأكد من أن لديك أحدث إصدار من واجهة سطر الأوامر (CLI). من المفترض أن يعمل هذا الدرس التطبيقي حول التعليمات البرمجية مع الإصدار 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
}
}
}
القاعدة الأولى التي ستكتبها للمسودات ستتحكم في من يمكنه إنشاء المستندات. في هذا التطبيق، لا يمكن إنشاء المسودات إلا بواسطة الشخص المدرج كمؤلف. تأكد من أن المعرف الفريد (UID) الخاص بالشخص الذي يقدم الطلب هو نفس المعرف الفريد (UID) المدرج في المستند.
الشرط الأول للإنشاء سيكون:
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. الخطوات التالية
تهانينا! لقد كتبت قواعد الأمان التي ساهمت في اجتياز جميع الاختبارات وتأمين التطبيق!
فيما يلي بعض المواضيع ذات الصلة للتعمق فيها بعد ذلك:
- منشور المدونة : كيفية مراجعة القواعد الأمنية للمراجعة البرمجية
- Codelab : السير عبر التطوير المحلي الأول باستخدام المحاكيات
- الفيديو : كيفية استخدام إعداد CI للاختبارات المستندة إلى المحاكي باستخدام إجراءات GitHub