שאילתה מאובטחת על נתונים

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

כללים הם לא מסננים

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

שאילתות וכללי אבטחה

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

אבטחה של מסמכים ושליחת שאילתות על סמך auth.uid

תוכלו להיעזר בדוגמה הבאה כדי לכתוב שאילתה כדי לאחזר מסמכים שמוגנים על ידי כלל אבטחה. נניח שיש מסד נתונים שמכיל אוסף של מסמכי story:

/story/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

בנוסף לשדות title ו-content, כל מסמך שומר את השדות author ו-published שמשמשים לבקרת גישה. בדוגמאות האלה, ההנחה היא שהאפליקציה משתמשת באימות ב-Firebase כדי להגדיר את השדה author כ-UID של המשתמש שיצר את המסמך. אימות Firebase מאכלס גם את המשתנה request.auth בכללי האבטחה.

כלל האבטחה הבא משתמש במשתנים request.auth ו-resource.data כדי להגביל את הגישה לקריאה ולכתיבה של כל story לכותב שלו:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

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

לא חוקי: אילוצי שאילתה לא תואמים לאילוצים של כללי אבטחה

// This query will fail
db.collection("stories").get()

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

לעומת זאת, השאילתה הבאה מצליחה, כי היא כוללת את אותה אילוץ על השדה author כמו כללי האבטחה:

חוקי: אילוצי שאילתה תואמים למגבלות של כללי אבטחה

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

אבטחה של מסמכים ושליחת שאילתות על סמך שדה

כדי להדגים יותר את האינטראקציה בין השאילתות לכללים, כללי האבטחה שבהמשך מרחיבים את גישת הקריאה של האוסף stories, כדי לאפשר לכל משתמש לקרוא מסמכי story שבהם השדה published מוגדר ל-true.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

השאילתה על הדפים שפורסמו חייבת לכלול את אותן מגבלות כמו כללי האבטחה:

db.collection("stories").where("published", "==", true).get()

אילוץ השאילתה .where("published", "==", true) מבטיח ש-resource.data.published יהיה true בכל תוצאה. לכן, השאילתה הזו עומדת בכללי האבטחה ויש לה הרשאה לקרוא נתונים.

OR שאילתות

כשמבצעים הערכה של שאילתה לוגית מסוג OR (or,‏ in או array-contains-any) מול קבוצת כללים, Cloud Firestore מעריך כל ערך השוואה בנפרד. כל ערך השוואה חייב לעמוד באילוצים של כללי האבטחה. לדוגמה, עבור הכלל הבא:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

לא חוקי: השאילתה לא מבטיחה ש-x > 5 לכל המסמכים הפוטנציאליים

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

תקינה: השאילתה מבטיחה ש-x > 5 לכל המסמכים הפוטנציאליים

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

בדיקת אילוצים על שאילתות

כללי האבטחה יכולים גם לאשר או לדחות שאילתות על סמך האילוצים שלהם. המשתנה request.query מכיל את המאפיינים limit, offset ו-orderBy של שאילתה. לדוגמה, כללי האבטחה יכולים לדחות כל שאילתה שלא מגבילה את מספר המסמכים המקסימלי שאפשר לאחזר לטווח מסוים:

allow list: if request.query.limit <= 10;

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

  • מערכת הכללים מפרידה את כלל הקריאה לכללים עבור get ו-list.
  • הכלל get מגביל את האחזור של מסמכים בודדים למסמכים ציבוריים או למסמכים שהמשתמש כתב.
  • הכלל list מחיל את אותן הגבלות כמו get, אבל על שאילתות. היא גם בודקת את מגבלת השאילתות ואז דוחה כל שאילתה בלי מגבלה או עם מגבלה גדולה מ-10.
  • מערכת הכללים מגדירה פונקציית authorOrPublished() כדי למנוע כפילות בקוד.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

שאילתות וכלל אבטחה של קבוצת אוספים

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

אבטחה של מסמכים ושליחת שאילתות על סמך קבוצות של אוספים

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

  1. חשוב לוודא שהשורה הראשונה בערכת הכללים היא rules_version = '2';. כדי להשתמש בשאילתות של קבוצות איסוף, צריך להשתמש בתו הכללי לחיפוש החדש הרקורסיבי {name=**} בגרסה 2 של כללי האבטחה.
  2. כותבים כלל לקבוצת האוספים באמצעות match /{path=**}/[COLLECTION_ID]/{doc}.

לדוגמה, נניח שיש פורום שמאורגן ב-forum מסמכים שמכילים posts אוספי משנה:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

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

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

כל משתמש מאומת יכול לאחזר את התגובות של כל פורום:

db.collection("forums/technology/posts").get()

אבל מה קורה אם רוצים להציג למשתמש הנוכחי את הפוסטים שלו בכל הפורומים? אפשר להשתמש בשאילתת קבוצת אוספים כדי לאחזר תוצאות מכל האוספים posts:

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

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

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

עם זאת, חשוב לשים לב שהכללים האלה יחולו על כל הקולקציות עם המזהה posts, ללא קשר להיררכיה. לדוגמה, הכללים האלה חלים על כל הקולקציות הבאות של posts:

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

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

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

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

לאחר מכן אפשר לכתוב כללים לקבוצת האוספים posts על סמך הסטטוס published והפוסט author:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

בעזרת הכללים האלה, לקוחות אינטרנט, לקוחות Apple ולקוחות Android יכולים לבצע את השאילתות הבאות:

  • כל אחד יכול לאחזר פוסטים שפורסמו בפורום:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • כל אחד יכול לאחזר את הפוסטים שפורסמו על ידי מחבר מסוים בכל הפורומים:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • מחברים יכולים לאחזר את כל הפוסטים שלהם, שפורסמו וגם שלא פורסמו, בכל הפורומים:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

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

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

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

‎/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

שימו לב לשדה user. למרות שאנחנו יודעים איזה משתמש הוא הבעלים של מסמך transaction מהנתיב של המסמך, אנחנו משכפלים את המידע הזה בכל מסמך transaction כי כך אנחנו יכולים לעשות שני דברים:

  • צריך לכתוב שאילתות של קבוצות אוספים שמוגבלות למסמכים שכוללים /users/{userid} ספציפי בנתיב המסמך. לדוגמה:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • לאכוף את ההגבלה הזו על כל השאילתות בקבוצת האוספים transactions, כך שמשתמש אחד לא יוכל לאחזר את מסמכי transaction של משתמש אחר.

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

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

השלבים הבאים