שליטה בגישה לשדות ספציפיים

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