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

הדף הזה מתבסס על המושגים יצירת כללי אבטחה כתיבת תנאים לכללי אבטחה כדי להסביר איך 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
    }
  }
}

השלבים הבאים