שליחת שאילתות לנתונים באופן מאובטח

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

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

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

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

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

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

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

/stories/{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. הסיבה להתנהגות הזו היא שכאשר 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) ביחס לקבוצת כללים, 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';. שאילתות של קבוצות אוספים דורשות את ההתנהגות של התווים החדשים של wildcard רקורסיבי {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;
    }
  }
}

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

  • לכתוב שאילתות של קבוצת אוספים שמוגבלות למסמכים שכוללים /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
    }
  }
}

השלבים הבאים