שליטה בגישה לשדות ספציפיים
הדף הזה מבוסס על המושגים שמוסברים במאמרים מבנה של כללי אבטחה וכתיבת תנאים לכללי אבטחה. כאן נסביר איך אפשר להשתמש בכללי אבטחה של Firestore כדי ליצור כללים שמאפשרים ללקוחות לבצע פעולות בשדות מסוימים במסמך, אבל לא בשדות אחרים.
יכול להיות שתרצו לשלוט בשינויים במסמך לא ברמת המסמך אלא ברמת השדה.
לדוגמה, יכול להיות שתרצו לאפשר ללקוח ליצור או לשנות מסמך, אבל לא לאפשר לו לערוך שדות מסוימים במסמך הזה. או שאולי תרצו לאכוף שכל מסמך שהלקוח יוצר תמיד יכיל קבוצה מסוימת של שדות. במדריך הזה מוסבר איך אפשר לבצע חלק מהמשימות האלה באמצעות כללי אבטחה ב-Firestore.
מתן הרשאת קריאה בלבד לשדות ספציפיים
קריאות ב-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'
}
}
}
}
הגבלת שדות ביצירת מסמך
Firestore הוא חסר סכימה, כלומר אין הגבלות ברמת מסד הנתונים לגבי השדות שמסמך מכיל. הגמישות הזו יכולה להקל על הפיתוח, אבל לפעמים תרצו לוודא שלקוחות יכולים ליצור רק מסמכים שמכילים שדות ספציפיים, או שלא מכילים שדות אחרים.
אפשר ליצור את הכללים האלה על ידי בדיקת השיטה keys של האובייקט request.resource.data. זו רשימה של כל השדות שהלקוח מנסה לכתוב במסמך החדש הזה. אם משלבים את קבוצת השדות הזו עם פונקציות כמו hasOnly() או hasAny(), אפשר להוסיף לוגיקה שמגבילה את סוגי המסמכים שמשתמש יכול להוסיף ל-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() בכללי האבטחה.
אכיפת סוגי שדות
השפעה נוספת של העובדה ש-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 לא קיים, מתקבלת שגיאה, ולכן כל כלל אבטחה שקורא לפונקציה הזו ידחה את הבקשה. כדי לטפל במצב הזה, אפשר להשתמש ב-method 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).
אסור לבצע כתיבה חלקית
הערה אחרונה לגבי כללי האבטחה של Firestore: הם מאפשרים ללקוח לבצע שינוי במסמך, או דוחים את כל העריכה. אי אפשר ליצור כללי אבטחה שמקבלים פעולות כתיבה לשדות מסוימים במסמך ודוחים פעולות כתיבה לשדות אחרים באותה פעולה.