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