הגן על נתוני Firestore שלך ​​עם כללי האבטחה של Firebase

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