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

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

קריאה ל-method‏ 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() בכללי האבטחה.

אכיפת סוגי שדות

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

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

לדוגמה, הכללים הבאים מוודאים ששדה 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).

אסור לכתוב בחלקים

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