שימוש בתנאים בכללי אבטחה של Cloud Storage ב-Firebase

המדריך הזה מבוסס על המדריך לימוד התחביר הבסיסי של שפת Firebase Security Rules, ומראה איך להוסיף תנאים ל-Firebase Security Rules של Cloud Storage.

אבן הבניין הראשית של Cloud Storage Security Rules היא התנאי. תנאי הוא ביטוי בוליאני שקובע אם יש לאפשר או לדחות פעולה מסוימת. בכלל פשוט, השימוש ב-literals של true ו-false בתור תנאים עובד מצוין. אבל השפה Firebase Security Rules for Cloud Storage מאפשרת לכם לכתוב תנאים מורכבים יותר שיכולים:

  • בדיקת אימות המשתמשים
  • אימות נתונים נכנסים

אימות

Firebase Security Rules ל-Cloud Storage משתלב עם Firebase Authentication כדי לספק ל-Cloud Storage אימות חזק שמבוסס על משתמשים. כך אפשר לבצע בקרת גישה מפורטת על סמך הצהרות של אסימון Firebase Authentication.

כשמשתמש מאומת מבצע בקשה נגד Cloud Storage, משתנה request.auth מאוכלס ב-uid של המשתמש (request.auth.uid) וגם בהצהרות של Firebase Authentication JWT (request.auth.token).

בנוסף, כשמשתמשים באימות מותאם אישית, הצהרות נוספות מופיעות בשדה request.auth.token.

כשמשתמש לא מאומת מבצע בקשה, המשתנה request.auth הוא null.

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

  • גלוי לכולם: התעלמות מ-request.auth
  • פרטי מאומת: בודקים ש-request.auth אינו null
  • פרטי משתמש: בודקים ש-request.auth.uid שווה לנתיב uid
  • פרטי קבוצה: בודקים אם הצהרות האסימון בהתאמה אישית תואמות להצהרה שנבחרה, או קוראים את המטא-נתונים של הקובץ כדי לראות אם קיים שדה מטא-נתונים

גלוי לכולם

כל כלל שלא מתייחס להקשר של request.auth יכול להיחשב ככלל public, כי הוא לא מתייחס להקשר האימות של המשתמש. הכללים האלה יכולים להיות שימושיים כדי להציג נתונים ציבוריים, כמו נכסי משחקים, קובצי קול או תוכן סטטי אחר.

// Anyone to read a public image if the file is less than 100kB
// Anyone can upload a public file ending in '.txt'
match /public/{imageId} {
  allow read: if resource.size < 100 * 1024;
  allow write: if imageId.matches(".*\\.txt");
}

פרטי מאומת

במקרים מסוימים, יכול להיות שתרצו שכל המשתמשים המאומתים באפליקציה יוכלו לראות את הנתונים, אבל לא משתמשים לא מאומתים. מאחר שהמשתנה request.auth הוא null לכל המשתמשים שלא אומתו, כל מה שצריך לעשות הוא לבדוק אם המשתנה request.auth קיים כדי לדרוש אימות:

// Require authentication on all internal image reads
match /internal/{imageId} {
  allow read: if request.auth != null;
}

פרטי למשתמש

התרחיש הנפוץ ביותר לשימוש ב-request.auth הוא מתן הרשאות מפורטות על הקבצים של משתמשים ספציפיים: החל מהעלאת תמונות פרופיל ועד לקריאת מסמכים פרטיים.

מכיוון שלקבצים ב-Cloud Storage יש 'נתיב' מלא לקובץ, כל מה שצריך כדי לשלוט בקובץ על ידי משתמש הוא קטע מידע ייחודי שמזהה את המשתמש בתחילית של שם הקובץ (כמו uid של המשתמש), שאפשר לבדוק כשבודקים את הכלל:

// Only a user can upload their profile picture, but anyone can view it
match /users/{userId}/profilePicture.png {
  allow read;
  allow write: if request.auth.uid == userId;
}

קבוצה פרטית

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

אחרי שהנתונים האלה מאוחסנים במטא-נתונים של האסימון או הקובץ, אפשר להפנות אליהם מתוך כלל:

// Allow reads if the group ID in your token matches the file metadata's `owner` property
// Allow writes if the group ID is in the user's custom token
match /files/{groupId}/{fileName} {
  allow read: if resource.metadata.owner == request.auth.token.groupId;
  allow write: if request.auth.token.groupId == groupId;
}

שליחת בקשה להערכה

המערכת מבצעת הערכה של העלאות, הורדות, שינויים במטא-נתונים ומחיקות באמצעות הערך של request שנשלח אל Cloud Storage. בנוסף למזהה הייחודי של המשתמש ולמטען הייעודי Firebase Authentication באובייקט request.auth כפי שמתואר למעלה, המשתנה request מכיל את נתיב הקובץ שבו מתבצעת הבקשה, את השעה שבה התקבלה הבקשה ואת הערך החדש של resource אם הבקשה היא לכתיבה.

האובייקט request מכיל גם את המזהה הייחודי של המשתמש ואת עומס העבודה (payload) של Firebase Authentication באובייקט request.auth. מידע נוסף זמין בקטע אבטחה מבוססת-משתמשים במסמכים.

בהמשך מופיעה רשימה מלאה של המאפיינים באובייקט request:

נכס סוג תיאור
auth map<string, string> כשמשתמש מחובר לחשבון, הוא מספק את uid, המזהה הייחודי של המשתמש, וגם את token, מפה של Firebase Authentication הצהרות JWT. אחרת, הערך יהיה null.
params map<string, string> מפה שמכילה את הפרמטרים של השאילתה בבקשה.
path נתיב path שמייצג את הנתיב שבו מתבצעת הבקשה.
resource map<string, string> ערך המשאב החדש, שמופיע רק בבקשות write.
time חותמת זמן חותמת זמן שמייצגת את השעה בשרת שבה הבקשה נבדקה.

הערכת משאבים

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

Firebase Security Rules עבור Cloud Storage מספק מטא-נתונים של קובץ באובייקט resource, שמכיל צמדי מפתח/ערך של המטא-נתונים שמוצגים באובייקט Cloud Storage. אפשר לבדוק את המאפיינים האלה בבקשות read או write כדי לוודא את תקינות הנתונים.

בבקשות write (כמו העלאות, עדכוני מטא-נתונים ומחיקה), בנוסף לאובייקט resource שמכיל את המטא-נתונים של הקובץ שקיים כרגע בנתיב הבקשה, יש לכם גם אפשרות להשתמש באובייקט request.resource שמכיל קבוצת משנה של המטא-נתונים של הקובץ שרוצים לכתוב אם הכתיבה מותרת. אפשר להשתמש בשני הערכים האלה כדי לוודא את תקינות הנתונים או לאכוף אילוצים באפליקציה, כמו סוג הקובץ או הגודל שלו.

בהמשך מופיעה רשימה מלאה של המאפיינים באובייקט resource:

נכס סוג תיאור
name string השם המלא של האובייקט
bucket string השם של הקטגוריה שבה האובייקט נמצא.
generation int מספר הגרסה של האובייקט Google Cloud Storage.
metageneration int מספר המטא-יצירה של האובייקט Google Cloud Storage.
size int גודל האובייקט בבייטים.
timeCreated חותמת זמן חותמת זמן שמייצגת את הזמן שבו נוצר אובייקט.
updated חותמת זמן חותמת זמן שמייצגת את הזמן שבו אובייקט עודכן בפעם האחרונה.
md5Hash string גיבוב (hash) MD5 של האובייקט.
crc32c string גיבוב CRC32C של האובייקט.
etag string ה-etag שמשויך לאובייקט הזה.
contentDisposition string אופן הטיפול בתוכן שמשויך לאובייקט הזה.
contentEncoding string קידוד התוכן שמשויך לאובייקט הזה.
contentLanguage string שפת התוכן שמשויכת לאובייקט הזה.
contentType string סוג התוכן שמשויך לאובייקט הזה.
metadata map<string, string> צמדי מפתח/ערך של מטא-נתונים מותאמים אישית נוספים שצוינו על ידי המפתח.

השדה request.resource מכיל את כל הערכים האלה, מלבד generation, ‏ metageneration, ‏ etag, ‏ timeCreated ו-updated.

שיפור באמצעות Cloud Firestore

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

באמצעות הפונקציות firestore.get() ו-firestore.exists(), כללי האבטחה יכולים להעריך בקשות נכנסות בהשוואה למסמכים ב-Cloud Firestore. הפונקציות firestore.get() ו-firestore.exists() מצפות לנתיבי מסמכים שצוינו במלואם. כשמשתמשים במשתנים כדי ליצור נתיב ל-firestore.get() ול-firestore.exists(), צריך להשתמש בבריחה מפורשת מהמשתנים באמצעות התחביר $(variable).

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

service firebase.storage {
  match /b/{bucket}/o {
    match /users/{club}/files/{fileId} {
      allow read: if club in
        firestore.get(/databases/(default)/documents/users/$(request.auth.id)).memberships
    }
  }
}
בדוגמה הבאה, רק החברים של המשתמש יכולים לראות את התמונות שלו.
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/photos/{fileId} {
      allow read: if
        firestore.exists(/databases/(default)/documents/users/$(userId)/friends/$(request.auth.id))
    }
  }
}

אחרי שיוצרים ושומרים את ה-Cloud Storage Security Rules הראשון שמשתמש בפונקציות ה-Cloud Firestore האלה, מופיעה בקשה במסוף Firebase או ב-CLI של Firebase להפעיל הרשאות כדי לחבר את שני המוצרים.

כדי להשבית את התכונה, מסירים תפקיד IAM, כפי שמתואר בקטע ניהול ופריסה של Firebase Security Rules.

אימות נתונים

אפשר להשתמש ב-Firebase Security Rules עבור Cloud Storage גם לאימות נתונים, כולל אימות שם הקובץ והנתיב שלו, וגם תכונות של מטא-נתונים של קובץ כמו contentType ו-size.

service firebase.storage {
  match /b/{bucket}/o {
    match /images/{imageId} {
      // Only allow uploads of any image file that's less than 5MB
      allow write: if request.resource.size < 5 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*');
    }
  }
}

פונקציות מותאמות אישית

ככל ש-Firebase Security Rules נעשה מורכב יותר, כדאי לתחום קבוצות של תנאים בפונקציות שאפשר לעשות בהן שימוש חוזר בכללי המדיניות. כללי האבטחה תומכים בפונקציות בהתאמה אישית. התחביר של פונקציות בהתאמה אישית דומה ל-JavaScript, אבל פונקציות Firebase Security Rules נכתבות בשפה ספציפית לדומיין שיש לה כמה מגבלות חשובות:

  • פונקציות יכולות להכיל רק משפט return אחד. הן לא יכולות להכיל לוגיקה נוספת. לדוגמה, הם לא יכולים להריץ לולאות או לבצע קריאה לשירותים חיצוניים.
  • פונקציות יכולות לגשת באופן אוטומטי לפונקציות ולמשתנים מההיקף שבו הן מוגדרות. לדוגמה, לפונקציה שמוגדרת בהיקף service firebase.storage יש גישה למשתנה resource, ולפונקציות מובנות כמו get() ו-exists() רק בהיקף Cloud Firestore.
  • פונקציות יכולות לקרוא לפונקציות אחרות, אבל אסור להן לבצע חזרה חוזרת (recursion). עומק ה-call stack הכולל מוגבל ל-10.
  • בגרסה rules2, אפשר להגדיר משתנים בפונקציות באמצעות מילת המפתח let. פונקציות יכולות לכלול כל מספר קישורי let, אבל הן חייבות להסתיים בהצהרת return.

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

service firebase.storage {
  match /b/{bucket}/o {
    // True if the user is signed in or the requested data is 'public'
    function signedInOrPublic() {
      return request.auth.uid != null || resource.data.visibility == 'public';
    }
    match /images/{imageId} {
      allow read, write: if signedInOrPublic();
    }
    match /mp3s/{mp3Ids} {
      allow read: if signedInOrPublic();
    }
  }
}

שימוש בפונקציות ב-Firebase Security Rules מאפשר לכם לשמור על קוד תקין ככל שהכללים שלכם נעשים מורכבים יותר.

השלבים הבאים

אחרי הדיון הזה על תנאים, יש לכם הבנה מעמיקה יותר של כללים ואתם מוכנים:

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