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

במהלך פיתוח האפליקציה, כדאי לנעול את הגישה למסד הנתונים Cloud Firestore שלכם. עם זאת, לפני ההשקה, תצטרכו להשתמש בCloud Firestore Security Rules מורכב יותר. בנוסף ליצירת אב טיפוס ובדיקה של התכונות הכלליות וההתנהגות של האפליקציה, בעזרת האמולטור Cloud Firestore תוכלו לכתוב בדיקות יחידה (unit testing) כדי לבדוק את ההתנהגות של 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 של לקוחות, ויעריכו את הכללים בהתאם. כך תוכלו לקשר את האפליקציה ישירות לאמולטורים בבדיקות ידניות ובשילוב.

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

הרצת בדיקות יחידה מקומית עם JavaScript SDK v9

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

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

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

ספריית v9 Rules Unit Testing תמיד מודעת לאמולטורים ואף פעם לא נוגעת במשאבי הייצור שלכם.

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

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.

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

בנוסף, כדאי לעיין בשיטות בדיקה ספציפיות לאמולטור ב-v9 SDK.

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

ה-method הזה יוצר RulesTestContext, שפועל כמו משתמש מאומת. לבקשות שנוצרו באמצעות ההקשר שמוחזר יהיה מצורף אסימון אימות מדומה. אפשר גם להעביר אובייקט שמגדיר הצהרות מותאמות אישית או שינוי של נתוני עומס עבודה (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

ה-method הזה יוצר 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()

ה-method הזה משמיד את כל 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 להקשר הבדיקה הזה. אפשר להשתמש במכונה של Client-SDK של Firebase JS שמוחזרת עם ממשקי ה-API של ה-SDK של הלקוח (גרסה מודולרית ב-v9 או תאימות ל-v9).

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

בעזרת המהדר 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, הוספנו את ה-method 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 מאפשר או דוחה מקרים שונים בצורה נכונה.