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