בדיקת כללי האבטחה של Cloud Firestore

במהלך פיתוח האפליקציה, מומלץ לנעול את הגישה למסד הנתונים Cloud Firestore. עם זאת, לפני ההשקה, תצטרכו לקבל Cloud Firestore Security Rules מפורט יותר. באמצעות המהדר של Cloud Firestore, בנוסף ליצירת אב טיפוס ובדיקת התכונות וההתנהגות הכלליות של האפליקציה, אפשר לכתוב בדיקות יחידה לבדיקת ההתנהגות של Cloud Firestore Security Rules.

מדריך למתחילים

כדי לקבל כמה תרחישי בדיקה בסיסיים עם כללים פשוטים, אפשר לנסות את הדוגמה למדריך למתחילים.

הסבר על Cloud Firestore Security Rules

כשמשתמשים בספריות הלקוח לנייד ולאינטרנט, אפשר להטמיע את Firebase Authentication ו-Cloud Firestore Security Rules לאימות, להרשאה ולתיקוף נתונים ללא שרת.

Cloud Firestore Security Rules כוללים שני חלקים:

  1. משפט match שמזהה מסמכים במסד הנתונים.
  2. ביטוי allow שקובע את הגישה למסמכים האלה.

Firebase Authentication מאמת את פרטי הכניסה של המשתמשים ומספק את הבסיס למערכות גישה מבוססות-משתמשים ומבוססות-תפקידים.

כל בקשה למסד נתונים מספריית לקוח לנייד או לאינטרנט של Cloud Firestore נבדקת מול כללי האבטחה שלכם לפני קריאה או כתיבת של נתונים. אם הכללים מונעים גישה לאחד מנתיבי המסמכים שצוינו, הבקשה כולה נכשלת.

מידע נוסף על Cloud Firestore Security Rules זמין במאמר תחילת העבודה עם Cloud Firestore Security Rules.

התקנת האמולטור

כדי להתקין את הסימולטור של Cloud Firestore, משתמשים ב-CLI של Firebase ומריצים את הפקודה הבאה:

firebase setup:emulators:firestore

הרצת האמולטור

בשלב הראשון, מפעילים פרויקט Firebase בספריית העבודה. זהו שלב ראשון נפוץ כשמשתמשים ב-Firebase CLI.

firebase init

מפעילים את הסימולטור באמצעות הפקודה הבאה. הסימולטור יפעל עד שתפסיקו את התהליך:

firebase emulators:start --only firestore

במקרים רבים, כדאי להפעיל את הסימולטור, להריץ חבילת בדיקות ואז לכבות את הסימולטור אחרי שהבדיקות מסתיימות. אפשר לעשות זאת בקלות באמצעות הפקודה emulators:exec:

firebase emulators:exec --only firestore "./my-test-script.sh"

כשהסימולטור יופעל, הוא ינסה לפעול ביציאת ברירת המחדל (8080). כדי לשנות את יציאת הסימולטור, משנים את הקטע "emulators" בקובץ firebase.json:

{
  // ...
  "emulators": {
    "firestore": {
      "port": "YOUR_PORT"
    }
  }
}

לפני שמפעילים את הסימולטור

לפני שמתחילים להשתמש באמולטור, חשוב לזכור את הדברים הבאים:

  • בשלב הראשון, הסימולטור יטען את הכללים שצוינו בשדה firestore.rules בקובץ firebase.json. הוא מצפה לשם של קובץ מקומי שמכיל את Cloud Firestore Security Rules, ומחילה את הכללים האלה על כל הפרויקטים. אם לא מציינים את נתיב הקובץ המקומי או משתמשים בשיטה loadFirestoreRules כפי שמתואר בהמשך, בכל הפרויקטים יופיעו כללים פתוחים במהלך ההרצה במהדורת האפליקציה של האפליקטור.
  • רוב ערכות ה-SDK של Firebase פועלות ישירות עם הסימולטורים, אבל רק הספרייה @firebase/rules-unit-testing תומכת בהדמיה של auth בכללי האבטחה, וכך מאפשרת לבצע בדיקות יחידה בקלות רבה יותר. בנוסף, הספרייה תומכת בכמה תכונות ספציפיות למהדר, כמו ניקוי כל הנתונים, כפי שמפורט בהמשך.
  • הסימולטורים יקבלו גם אסימוני אימות של Firebase בסביבת הייצור שסופקו דרך ערכות ה-SDK של הלקוח, ויעריכו את הכללים בהתאם. כך תוכלו לחבר את האפליקציה ישירות לסימולטורים במהלך שילוב ובמהלך בדיקות ידניות.

הרצת בדיקות יחידה מקומיות

הרצת בדיקות יחידה מקומיות באמצעות ה-SDK של JavaScript בגרסה 9

Firebase מפיץ ספרייה לבדיקת יחידות של כללי אבטחה, עם ערכת ה-SDK של JavaScript בגרסה 9 ועם ערכת ה-SDK בגרסה 8. ממשקי ה-API של הספריות שונים באופן משמעותי. מומלץ להשתמש בספריית הבדיקה בגרסה 9, שהיא יעילה יותר ומחייבת פחות הגדרות כדי להתחבר למהדמנים, וכך אפשר להימנע באופן בטוח משימוש בטעות במשאבי הייצור. מטעמי תאימות לאחור, אנחנו ממשיכים להציע את ספריית הבדיקות של v8.

משתמשים במודול @firebase/rules-unit-testing כדי ליצור אינטראקציה עם הסימולטור שפועל באופן מקומי. אם מופיעות הודעות זמן קצוב פג או שגיאות מסוג ECONNREFUSED, צריך לוודא שהאמולטור פועל בפועל.

מומלץ מאוד להשתמש בגרסה עדכנית של Node.js כדי שתוכלו להשתמש בסימון async/await. כמעט כל ההתנהגות שרוצים לבדוק כוללת פונקציות אסינכרוניות, ומודול הבדיקה מיועד לעבוד עם קוד שמבוסס על Promise.

ספריית בדיקות היחידה של הכללים בגרסה 9 תמיד מודעת למהדמנים, והיא אף פעם לא נוגעת במשאבי הייצור שלכם.

מייבאים את הספרייה באמצעות הצהרות ייבוא מודולריות בגרסה 9. לדוגמה:

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment
} from "@firebase/rules-unit-testing"

// Use `const { … } = require("@firebase/rules-unit-testing")` if imports are not supported
// Or we suggest `const testing = require("@firebase/rules-unit-testing")` if necessary.

לאחר הייבוא, כדי להטמיע בדיקות יחידה צריך:

  • יצירת RulesTestEnvironment והגדרתו באמצעות קריאה ל-initializeTestEnvironment.
  • הגדרת נתוני בדיקה בלי להפעיל כללים, באמצעות שיטה נוחה שמאפשרת לעקוף אותם באופן זמני, RulesTestEnvironment.withSecurityRulesDisabled.
  • הגדרת חבילה של בדיקות וכל בדיקה בנפרד עם קוד לפני/אחרי (hook) עם קריאות לניקוי נתוני הבדיקה והסביבה, כמו RulesTestEnvironment.cleanup() או RulesTestEnvironment.clearFirestore().
  • הטמעת תרחישי בדיקה שמחקים מצבי אימות באמצעות RulesTestEnvironment.authenticatedContext ו-RulesTestEnvironment.unauthenticatedContext.

שיטות נפוצות ופונקציות שירות

אפשר גם לעיין במאמר שיטות בדיקה ספציפיות למהדר בסביבת ה-SDK בגרסה 9.

initializeTestEnvironment() => RulesTestEnvironment

הפונקציה הזו מאתחלת סביבה לבדיקה של כללי יחידה. צריך להפעיל את הפונקציה הזו קודם כדי להגדיר את הבדיקה. כדי שההרצה תתבצע בהצלחה, צריך להפעיל את האמולטורים.

הפונקציה מקבלת אובייקט אופציונלי שמגדיר TestEnvironmentConfig, שיכול לכלול מזהה פרויקט והגדרות תצורה של אמולטור.

let testEnv = await initializeTestEnvironment({
  projectId: "demo-project-1234",
  firestore: {
    rules: fs.readFileSync("firestore.rules", "utf8"),
  },
});

RulesTestEnvironment.authenticatedContext({ user_id: string, tokenOptions?: TokenOptions }) => RulesTestContext

השיטה הזו יוצרת RulesTestContext, שמתנהג כמו משתמש מאומת של Authentication. לבקשות שנוצרות באמצעות ההקשר המוחזר יצורף טוקן אימות מדומה. אפשר גם להעביר אובייקט שמגדיר הצהרות מותאמות אישית או שינוי של נתוני עומס עבודה (payload) של אסימון אימות.

אפשר להשתמש באובייקט ההקשר של הבדיקה שהוחזר בבדיקות כדי לגשת לכל מכונות האמולטור שהוגדרו, כולל אלה שהוגדרו באמצעות initializeTestEnvironment.

// Assuming a Firestore app and the Firestore emulator for this example
import { setDoc } from "firebase/firestore";

const alice = testEnv.authenticatedContext("alice", { … });
// Use the Firestore instance associated with this context
await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

RulesTestEnvironment.unauthenticatedContext() => RulesTestContext

השיטה הזו יוצרת RulesTestContext, שמתנהג כמו לקוח שלא נכנס באמצעות אימות. לבקשות שנוצרות באמצעות ההקשר שהוחזר לא יהיו מצורפים אסימוני אימות של Firebase.

אפשר להשתמש באובייקט ההקשר של הבדיקה שהוחזר בבדיקות כדי לגשת לכל מכונות האמולטור שהוגדרו, כולל אלה שהוגדרו באמצעות initializeTestEnvironment.

// Assuming a Cloud Storage app and the Storage emulator for this example
import { getStorage, ref, deleteObject } from "firebase/storage";

const alice = testEnv.unauthenticatedContext();

// Use the Cloud Storage instance associated with this context
const desertRef = ref(alice.storage(), 'images/desert.jpg');
await assertSucceeds(deleteObject(desertRef));

RulesTestEnvironment.withSecurityRulesDisabled()

מריצים פונקציית הגדרה לבדיקה עם הקשר שפועל כאילו כללי האבטחה הושבתו.

השיטה מקבלת פונקציית קריאה חוזרת, שמקבלת את ההקשר של עקיפת כללי האבטחה ומחזירה אובייקט promise. ההקשר ייהרס ברגע שההבטחה תיפתר או תידחה.

RulesTestEnvironment.cleanup()

השיטה הזו משמידה את כל ה-RulesTestContexts שנוצרו בסביבת הבדיקה ומנקה את המשאבים הבסיסיים, כדי לאפשר יציאה נקייה.

השיטה הזו לא משנה את המצב של המהדמנים בשום צורה. כדי לאפס את הנתונים בין בדיקות, משתמשים בשיטה לניקוי נתונים ספציפית לאמולטור של האפליקציה.

assertSucceeds(pr: Promise<any>)) => Promise<any>

זוהי פונקציית שירות של מקרה בדיקה.

הפונקציה קובעת שה-Promise שסופק ועוטף פעולת אמולטור ייפתר ללא הפרות של כללי האבטחה.

await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

assertFails(pr: Promise<any>)) => Promise<any>

זוהי פונקציית שירות של מקרה בדיקה.

הפונקציה קובעת שה-Promise שסופק ועוטף פעולת אמולטור יידחה עם הפרה של כללי האבטחה.

await assertFails(setDoc(alice.firestore(), '/users/bob'), { ... });

שיטות ספציפיות למהדר

אפשר גם לעיין במאמר שימוש בשיטות בדיקה נפוצות ובפונקציות שירות ב-SDK בגרסה 9.

RulesTestEnvironment.clearFirestore() => Promise<void>

השיטה הזו מנקה נתונים במסד הנתונים של Firestore ששייכים ל-projectId שהוגדר למהדר של Firestore.

RulesTestContext.firestore(settings?: Firestore.FirestoreSettings) => Firestore;

השיטה הזו מקבלת מופע של Firestore להקשר הבדיקה הזה. אפשר להשתמש במכונה שחוזרת של Firebase JS Client SDK עם ממשקי ה-API של ה-Client SDK (מודולרי בגרסה 9 או תואם בגרסה 9).

הצגה חזותית של הערכות הכללים

בעזרת המהדרר Cloud Firestore אפשר להציג חזותית בקשות של לקוחות בממשק המשתמש של Emulator Suite, כולל מעקב אחר הערכה של כללי האבטחה של Firebase.

פותחים את הכרטיסייה Firestore > Requests כדי לראות את רצף ההערכה המפורט של כל בקשה.

מוניטור הבקשות של Firestore Emulator שמוצגות בו הערכות של כללי האבטחה

יצירת דוחות בדיקה

אחרי שמריצים חבילת בדיקות, אפשר לגשת לדוחות כיסוי בדיקות שבהם מוצגת האופן שבו כל אחד מכללי האבטחה שלכם נבדק.

כדי לקבל את הדוחות, שולחים שאילתה לנקודת קצה חשופה במהלך ההפעלה של הסימולטור. כדי לקבל גרסה שמתאימה לדפדפנים, צריך להשתמש בכתובת ה-URL הבאה:

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html

הפעולה הזו מפרקת את הכללים לביטויים ולביטויים משנה, שאפשר להעביר מעליהם את העכבר כדי לקבל מידע נוסף, כולל מספר הבדיקות והערכים שהוחזרו. כדי לקבל את גרסת ה-JSON הגולמי של הנתונים האלה, צריך לכלול את כתובת ה-URL הבאה בשאילתה:

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage

ההבדלים בין הסימולטור לסביבת הייצור

  1. אין צורך ליצור פרויקט Cloud Firestore באופן מפורש. כל מכונה שניגשים אליה נוצרת באופן אוטומטי במהלך ההפעלה של הסימולטור.
  2. הסימולטור של Cloud Firestore לא פועל עם התהליך הרגיל של Firebase Authentication. במקום זאת, ב-Firebase Test SDK סיפקנו את השיטה initializeTestApp() בספרייה rules-unit-testing, שמקבלת שדה auth. הכינוי ב-Firebase שנוצר באמצעות השיטה הזו יתנהג כאילו הוא אומת ככל ישות שתספקו. אם מעבירים את הערך null, הוא יתנהג כמשתמש לא מאומת (לדוגמה, כללי auth != null יכשלו).

פתרון בעיות מוכרות

כשמשתמשים במהדמה של Cloud Firestore, יכול להיות שתתקלו בבעיות הידועות הבאות. כדי לפתור בעיות התנהגות לא סדירה, פועלים לפי ההוראות הבאות. ההערות האלה נכתבו בהתאם לספרייה של בדיקות היחידה של כללי האבטחה, אבל הגישות הכלליות רלוונטיות לכל ערכת SDK של Firebase.

התנהגות הבדיקה לא עקבית

אם לפעמים הבדיקות עוברות ולפעמים הן נכשלות, גם בלי ששיניתם את הבדיקות עצמן, יכול להיות שצריך לוודא שהן מסודרות בצורה נכונה. רוב האינטראקציות עם הסימולטור הן אסינכרוניות, לכן חשוב לוודא שכל הקוד האסינכרוני ממוספר כראוי. אפשר לתקן את הסדר על ידי שרשור הבטחות או שימוש נרחב בסימון await.

כדאי לבדוק במיוחד את הפעולות האסינכרוניות הבאות:

  • הגדרת כללי אבטחה, למשל initializeTestEnvironment.
  • קריאה וכתיבה של נתונים, למשל באמצעות db.collection("users").doc("alice").get().
  • טענות נכוֹנוּת תפעוליות, כולל assertSucceeds ו-assertFails.

הבדיקות עוברות רק בפעם הראשונה שטענת את האמולטור

לאמולטור יש מצב. הוא מאחסן את כל הנתונים שנכתבו בו בזיכרון, כך שכל הנתונים הולכים לאיבוד כשהמכונה הווירטואלית מושבתת. אם מריצים כמה בדיקות עם אותו מזהה פרויקט, כל בדיקה יכולה לייצר נתונים שעשויים להשפיע על בדיקות הבאות. אתם יכולים להשתמש באחת מהשיטות הבאות כדי לעקוף את ההתנהגות הזו:

  • להשתמש במזהי פרויקטים ייחודיים לכל בדיקה. חשוב לזכור: אם תבחרו לעשות זאת, תצטרכו להפעיל את initializeTestEnvironment כחלק מכל בדיקה. הכללים נטענים באופן אוטומטי רק עבור מזהה הפרויקט שמוגדר כברירת מחדל.
  • לשנות את המבנה של הבדיקות כך שלא יהיו להן אינטראקציות עם נתונים שנכתבו בעבר (לדוגמה, להשתמש בקולקציה שונה לכל בדיקה).
  • מחיקה של כל הנתונים שנכתבו במהלך הבדיקה.

הגדרת הבדיקה מורכבת מאוד

כשמגדירים את הבדיקה, יכול להיות שתרצו לשנות את הנתונים באופן שלא מאפשרים Cloud Firestore Security Rules בפועל. אם הכללים שלכם הופכים את הגדרת הבדיקה ליותר מורכבת, נסו להשתמש ב-RulesTestEnvironment.withSecurityRulesDisabled בשלבים של ההגדרה, כדי שהפעולות של קריאה וכתיבה לא יגרמו לשגיאות PERMISSION_DENIED.

לאחר מכן, הבדיקה יכולה לבצע פעולות כמשתמש מאומת או לא מאומת באמצעות RulesTestEnvironment.authenticatedContext ו-unauthenticatedContext, בהתאמה. כך תוכלו לוודא שהפונקציה Cloud Firestore Security Rules מאשרת או דוחה בקשות בצורה נכונה.