בדף הזה נסביר איך אפשר להשתמש ב-Cloud Firestore Security Rules כדי ליצור כללים שמאפשרים ללקוחות לבצע פעולות בשדות מסוימים במסמך, אבל לא בשדות אחרים. הדף הזה מבוסס על המושגים שמוסברים במאמרים מבנה של כללי אבטחה וכתיבת תנאים לכללי אבטחה.
יכול להיות שתרצו לשלוט בשינויים במסמך לא ברמת המסמך אלא ברמת השדה.
לדוגמה, יכול להיות שתרצו לאפשר ללקוח ליצור או לשנות מסמך, אבל לא לאפשר לו לערוך שדות מסוימים במסמך הזה. או שאולי תרצו לאכוף שכל מסמך שלקוח יוצר תמיד יכיל קבוצה מסוימת של שדות. במדריך הזה מוסבר איך לבצע חלק מהמשימות האלה באמצעות Cloud Firestore Security Rules.
מתן הרשאת קריאה בלבד לשדות ספציפיים
קריאות ב-Cloud Firestore מתבצעות ברמת המסמך. או שמאחזרים את המסמך המלא, או שלא מאחזרים כלום. אי אפשר לאחזר מסמך חלקי. אי אפשר להשתמש רק בכללי אבטחה כדי למנוע ממשתמשים לקרוא שדות ספציפיים במסמך.
אם יש שדות מסוימים במסמך שאתם רוצים להסתיר ממשתמשים מסוימים, הדרך הכי טובה היא להעביר אותם למסמך נפרד. לדוגמה, אפשר ליצור מסמך בprivate
אוסף משנה באופן הבא:
/employees/{emp_id}
name: "Alice Hamilton",
department: 461,
start_date: <timestamp>
/employees/{emp_id}/private/finances
salary: 80000,
bonus_mult: 1.25,
perf_review: 4.2
אחר כך תוכלו להוסיף כללי אבטחה עם רמות גישה שונות לשתי הקולקציות. בדוגמה הזו אנחנו משתמשים בטענות מותאמות אישית לאימות
כדי לציין שרק משתמשים עם הטענה המותאמת אישית לאימות role
ששווה ל-Finance
יכולים
לראות את המידע הפיננסי של העובד.
service cloud.firestore {
match /databases/{database}/documents {
// Allow any logged in user to view the public employee data
match /employees/{emp_id} {
allow read: if request.resource.auth != null
// Allow only users with the custom auth claim of "Finance" to view
// the employee's financial data
match /private/finances {
allow read: if request.resource.auth &&
request.resource.auth.token.role == 'Finance'
}
}
}
}
הגבלת שדות ביצירת מסמכים
Cloud Firestore הוא חסר סכימה, כלומר אין הגבלות ברמת מסד הנתונים לגבי השדות שמסמך יכול להכיל. הגמישות הזו יכולה להקל על הפיתוח, אבל לפעמים תרצו לוודא שללקוחות תהיה אפשרות ליצור רק מסמכים שמכילים שדות ספציפיים, או שלא מכילים שדות אחרים.
כדי ליצור את הכללים האלה, צריך לבדוק את השיטה keys
של האובייקט request.resource.data
. זו רשימה של כל השדות שהלקוח מנסה לכתוב במסמך החדש. אם משלבים את קבוצת השדות הזו עם פונקציות כמו hasOnly()
או hasAny()
, אפשר להוסיף לוגיקה שמגבילה את סוגי המסמכים שמשתמש יכול להוסיף ל-Cloud Firestore.
דרישה של שדות ספציפיים במסמכים חדשים
נניח שרוצים לוודא שכל המסמכים שנוצרו באוסף restaurant
מכילים לפחות את השדות name
, location
ו-city
. אפשר לעשות את זה על ידי קריאה ל-hasAll()
ברשימת המפתחות במסמך החדש.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document contains a name
// location, and city field
match /restaurant/{restId} {
allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
}
}
}
כך אפשר ליצור מסעדות עם שדות אחרים גם כן, אבל מוודאים שכל המסמכים שנוצרו על ידי לקוח מכילים לפחות את שלושת השדות האלה.
איסור שימוש בשדות ספציפיים במסמכים חדשים
באופן דומה, אפשר למנוע מלקוחות ליצור מסמכים שמכילים שדות ספציפיים באמצעות hasAny()
מול רשימה של שדות אסורים. השיטה הזו מחזירה True אם מסמך מכיל אחד מהשדות האלה, ולכן כדאי לשלול את התוצאה כדי לאסור שימוש בשדות מסוימים.
לדוגמה, בדוגמה הבאה, ללקוחות אין הרשאה ליצור מסמך שמכיל את השדה average_score
או rating_count
, כי השדות האלה יתווספו על ידי קריאה לשרת בשלב מאוחר יותר.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document does *not*
// contain an average_score or rating_count field.
match /restaurant/{restId} {
allow create: if (!request.resource.data.keys().hasAny(
['average_score', 'rating_count']));
}
}
}
יצירת רשימת היתרים של שדות למסמכים חדשים
במקום לאסור שימוש בשדות מסוימים במסמכים חדשים, אפשר ליצור רשימה של השדות שמותרים במסמכים חדשים. אחר כך אפשר להשתמש בפונקציה hasOnly()
כדי לוודא שכל המסמכים החדשים שייווצרו יכילו רק את השדות האלה (או קבוצת משנה של השדות האלה) ולא שדות אחרים.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document doesn't contain
// any fields besides the ones listed below.
match /restaurant/{restId} {
allow create: if (request.resource.data.keys().hasOnly(
['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
שילוב של שדות חובה ושדות אופציונליים
אפשר לשלב פעולות של hasAll
ושל hasOnly
בכללי האבטחה כדי לדרוש חלק מהשדות ולאפשר אחרים. לדוגמה, בדוגמה הזו נדרש שכל המסמכים החדשים יכללו את השדות name
, location
ו-city
, ויכולים לכלול גם את השדות address
, hours
ו-cuisine
.
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document has a name,
// location, and city field, and optionally address, hours, or cuisine field
match /restaurant/{restId} {
allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
(request.resource.data.keys().hasOnly(
['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
בתרחיש מהעולם האמיתי, יכול להיות שתרצו להעביר את הלוגיקה הזו לפונקציית עזר כדי להימנע משכפול הקוד ולשלב בקלות רבה יותר את השדות האופציונליים והנדרשים לרשימה אחת, כמו בדוגמה הבאה:
service cloud.firestore {
match /databases/{database}/documents {
function verifyFields(required, optional) {
let allAllowedFields = required.concat(optional);
return request.resource.data.keys().hasAll(required) &&
request.resource.data.keys().hasOnly(allAllowedFields);
}
match /restaurant/{restId} {
allow create: if verifyFields(['name', 'location', 'city'],
['address', 'hours', 'cuisine']);
}
}
}
הגבלת שדות בעדכון
שיטת אבטחה נפוצה היא לאפשר ללקוחות לערוך רק חלק מהשדות ולא אחרים. אי אפשר לעשות את זה רק על ידי עיון ברשימת request.resource.data.keys()
השינויים שמתוארת בקטע הקודם, כי הרשימה הזו מייצגת את המסמך המלא כפי שהוא ייראה אחרי העדכון, ולכן היא תכלול שדות שהלקוח לא שינה.
עם זאת, אם תשתמשו בפונקציה diff()
, תוכלו להשוות בין request.resource.data
לבין האובייקט resource.data
, שמייצג את המסמך במסד הנתונים לפני העדכון. כך נוצר אובייקט mapDiff
, שהוא אובייקט שמכיל את כל השינויים בין שתי מפות שונות.
אפשר להשתמש בשיטה affectedKeys()
כדי ליצור קבוצה של שדות ששונו בעריכה. אחר כך תוכלו להשתמש בפונקציות כמו hasOnly()
או hasAny()
כדי לוודא שהקבוצה הזו מכילה פריטים מסוימים (או לא מכילה אותם).
מניעת שינוי של שדות מסוימים
באמצעות השיטה hasAny()
במערך שנוצר על ידי affectedKeys()
, ולאחר מכן ביטול התוצאה, אפשר לדחות כל בקשת לקוח שמנסה לשנות שדות שלא רוצים לשנות.
לדוגמה, יכול להיות שתרצו לאפשר ללקוחות לעדכן מידע על מסעדה, אבל לא לשנות את הציון הממוצע שלה או את מספר הביקורות.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Allow the client to update a document only if that document doesn't
// change the average_score or rating_count fields
allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['average_score', 'rating_count']));
}
}
}
איך מאפשרים לשנות רק שדות מסוימים
במקום לציין שדות שלא רוצים לשנות, אפשר גם להשתמש בפונקציה hasOnly()
כדי לציין רשימה של שדות שכן רוצים לשנות. באופן כללי, השיטה הזו נחשבת בטוחה יותר כי כתיבה לשדות חדשים במסמך אסורה כברירת מחדל, עד שמאשרים אותה באופן מפורש בכללי האבטחה.
לדוגמה, במקום לאסור את השדות average_score
ו-rating_count
, אפשר ליצור כללי אבטחה שמאפשרים ללקוחות לשנות רק את השדות name
, location
, city
, address
, hours
ו-cuisine
.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Allow a client to update only these 6 fields in a document
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
}
}
}
כלומר, אם בעתיד תהיה גרסה חדשה של האפליקציה שבה מסמכי המסעדות יכללו את השדה telephone
, ניסיונות לערוך את השדה הזה ייכשלו עד שתחזרו ותוסיפו את השדה לרשימה hasOnly()
בכללי האבטחה.
אכיפת סוגי שדות
השפעה נוספת של העובדה ש-Cloud Firestore הוא חסר סכימה היא שאין אכיפה ברמת מסד הנתונים לגבי סוגי הנתונים שאפשר לאחסן בשדות ספציפיים. אפשר לאכוף את זה בכללי אבטחה באמצעות האופרטור is
.
לדוגמה, כלל האבטחה הבא מחייב שהשדה score
של הביקורת יהיה מספר שלם, שהשדות headline
, content
ו-author_name
יהיו מחרוזות, ושהשדה review_date
יהיה חותמת זמן.
service cloud.firestore {
match /databases/{database}/documents {
match /restaurant/{restId} {
// Restaurant rules go here...
match /review/{reviewId} {
allow create: if (request.resource.data.score is int &&
request.resource.data.headline is string &&
request.resource.data.content is string &&
request.resource.data.author_name is string &&
request.resource.data.review_date is timestamp
);
}
}
}
}
סוגי הנתונים התקפים לאופרטור is
הם bool
, bytes
, float
, int
, list
, latlng
, number
, path
, map
, string
ו-timestamp
. האופרטור is
תומך גם בסוגי הנתונים constraint
, duration
, set
ו-map_diff
, אבל מכיוון שהם נוצרים על ידי השפה של כללי האבטחה עצמה ולא על ידי לקוחות, השימוש בהם נדיר ברוב האפליקציות המעשיות.
סוגי הנתונים list
ו-map
לא תומכים בגנריות או בארגומנטים של סוגים.
במילים אחרות, אפשר להשתמש בכללי אבטחה כדי לוודא ששדה מסוים מכיל רשימה או מפה, אבל אי אפשר לוודא ששדה מכיל רשימה של כל המספרים השלמים או כל המחרוזות.
באופן דומה, אפשר להשתמש בכללי אבטחה כדי לאכוף ערכי סוג עבור רשומות ספציפיות ברשימה או במיפוי (באמצעות סימון סוגריים או שמות מפתחות בהתאמה), אבל אין קיצור דרך לאכיפת סוגי הנתונים של כל הפריטים במיפוי או ברשימה בבת אחת.
לדוגמה, הכללים הבאים מוודאים ששדה tags
במסמך מכיל רשימה, והערך הראשון הוא מחרוזת. היא גם מוודאת שהשדה product
מכיל מפה, שמכילה בתורה שם מוצר שהוא מחרוזת וכמות שהיא מספר שלם.
service cloud.firestore {
match /databases/{database}/documents {
match /orders/{orderId} {
allow create: if request.resource.data.tags is list &&
request.resource.data.tags[0] is string &&
request.resource.data.product is map &&
request.resource.data.product.name is string &&
request.resource.data.product.quantity is int
}
}
}
}
צריך לאכוף את סוגי השדות גם כשיוצרים מסמך וגם כשמעדכנים אותו. לכן, כדאי לשקול ליצור פונקציית עזר שאפשר לקרוא לה גם בקטע היצירה וגם בקטע העדכון של כללי האבטחה.
service cloud.firestore {
match /databases/{database}/documents {
function reviewFieldsAreValidTypes(docData) {
return docData.score is int &&
docData.headline is string &&
docData.content is string &&
docData.author_name is string &&
docData.review_date is timestamp;
}
match /restaurant/{restId} {
// Restaurant rules go here...
match /review/{reviewId} {
allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
// Other rules may go here
allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
// Other rules may go here
}
}
}
}
החלת סוגים על שדות אופציונליים
חשוב לזכור שאם קוראים לפונקציה request.resource.data.foo
במסמך שבו foo
לא קיים, מתקבלת שגיאה, ולכן כל כלל אבטחה שמבצע את הקריאה הזו ידחה את הבקשה. כדי לטפל במצב הזה, אפשר להשתמש בשיטה get
ב-request.resource.data
. השיטה get
מאפשרת לספק ארגומנט ברירת מחדל לשדה שמאוחזר ממפה, אם השדה הזה לא קיים.
לדוגמה, אם מסמכי הבדיקה מכילים גם שדה אופציונלי photo_url
ושדה אופציונלי tags
שרוצים לוודא שהם מחרוזות ורשימות בהתאמה, אפשר לעשות זאת על ידי כתיבה מחדש של הפונקציה reviewFieldsAreValidTypes
כך שתיראה בערך כך:
function reviewFieldsAreValidTypes(docData) {
return docData.score is int &&
docData.headline is string &&
docData.content is string &&
docData.author_name is string &&
docData.review_date is timestamp &&
docData.get('photo_url', '') is string &&
docData.get('tags', []) is list;
}
הפעולה הזו דוחה מסמכים שבהם קיים tags
אבל הוא לא רשימה, אבל עדיין מאפשרת מסמכים שלא מכילים את השדה tags
(או photo_url
).
אסור לבצע כתיבה חלקית
הערה אחרונה לגבי Cloud Firestore Security Rules: הם מאפשרים ללקוח לבצע שינוי במסמך, או דוחים את כל העריכה. אי אפשר ליצור כללי אבטחה שמקבלים פעולות כתיבה לשדות מסוימים במסמך ודוחים פעולות כתיבה לשדות אחרים באותה פעולה.