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
ל-false.
הערות
- פוסטים שפורסמו מאפשרים תגובות, שהן אוסף משנה על כל פוסט שפורסם.
- כדי להפחית ניצול לרעה, המשתמשים חייבים להיות בעלי כתובת אימייל מאומתת ולא להיות ב-Denyist כדי להשאיר תגובה.
- ניתן לעדכן תגובות רק תוך שעה לאחר פרסומן.
- ניתן למחוק תגובות על ידי מחבר התגובה, מחבר הפוסט המקורי או על ידי מנחים.
בנוסף לכללי גישה, תיצור כללי אבטחה האוכפים שדות נדרשים ואימות נתונים.
הכל יקרה באופן מקומי, באמצעות 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
חבילת האמולטור שתשתמש בה כדי להפעיל את הבדיקות היא חלק מ-Firebase CLI (ממשק שורת פקודה) שניתן להתקין במחשב שלך עם הפקודה הבאה:
$ npm install -g firebase-tools
לאחר מכן, אשר שיש לך את הגרסה העדכנית ביותר של ה-CLI. מעבד קוד זה אמור לעבוד עם גרסה 8.4.0 ומעלה, אך גרסאות מאוחרות יותר כוללות תיקוני באגים נוספים.
$ firebase --version 9.10.2
3. הפעל את הבדיקות
בחלק זה, תפעיל את הבדיקות באופן מקומי. זה אומר שהגיע הזמן לאתחל את חבילת האמולטור.
הפעל את האמולטורים
לאפליקציה שתעבוד איתה יש שלושה אוספים עיקריים של Firestore: drafts
מכילות פוסטים בבלוג שנמצאים בתהליך, האוסף published
מכיל את הפוסטים בבלוג שפורסמו, comments
הן אוסף משנה של פוסטים שפורסמו. ה-repo מגיע עם בדיקות יחידה עבור כללי האבטחה המגדירים את תכונות המשתמש ושאר התנאים הנדרשים למשתמש ליצור, לקרוא, לעדכן ולמחוק מסמכים 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
. כל הפונקציות חייבות להסתיים במשפט return, והפונקציה שלנו תחזיר ערך בוליאני המציין אם אחד מהמשתנהים הוא נכון:
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 Actions