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

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

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

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

הבנה של Cloud Firestore Security Rules

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

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

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

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

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

מידע נוסף על 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 Auth בסביבת הייצור שסופקו דרך ערכות SDK של הלקוח, ויעריכו את הכללים בהתאם. כך אפשר לחבר את האפליקציה ישירות לאמולטורים בשילוב ובבדיקות ידניות.

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

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

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

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

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

ספריית Rules Unit Testing בגרסה 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.
  • הגדרת חבילת מקרים לבדיקה ו-hooks לפני/אחרי לכל בדיקה עם קריאות לניקוי נתוני הבדיקה והסביבה, כמו 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. לבקשות שנוצרו באמצעות ההקשר שהוחזר יצורף אסימון אימות מדומה. אפשר גם להעביר אובייקט שמגדיר טענות נכונות (claims) בהתאמה אישית או שינויים של המטען הייעודי (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().doc('/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 שמציג הערכות של כללי אבטחה

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

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

כדי לקבל את הדוחות, שולחים שאילתה לנקודת קצה (endpoint) חשופה באמולטור בזמן שהוא פועל. כדי לקבל גרסה שמתאימה לדפדפן, משתמשים בכתובת ה-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, סיפקנו את ה-method‏ initializeTestApp() בספרייה rules-unit-testing, שמקבלת שדה auth. ה-handle של Firebase שנוצר באמצעות השיטה הזו יתנהג כאילו הוא אומת בהצלחה כישות שסיפקתם. אם תעבירו את הערך null, הוא יתנהג כמו משתמש לא מאומת (לדוגמה, כללי auth != null ייכשלו).

פתרון בעיות ידועות

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

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

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

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

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

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

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

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

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

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

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