גישה מאובטחת לנתונים של משתמשים וקבוצות

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

הפתרון: בקרת גישה המבוססת על תפקידים (RBAC)

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

נניח שאתם מפתחים אפליקציה לכתיבה משותפת, שבה המשתמשים יכולים ליצור "סיפורים" ו "תגובות" עם דרישות האבטחה הבאות:

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

מבנה הנתונים

נניח שבאפליקציה יש אוסף stories שבו כל מסמך מייצג סיפור. לכל סיפור יש גם אוסף משנה בשם comments, שבו כל מסמך הוא תגובה על הסיפור הזה.

כדי לעקוב אחרי תפקידי הגישה, מוסיפים שדה roles שהוא מפה של מזהי משתמשים לתפקידים:

‎/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

התגובות מכילות רק שני שדות, מזהה המשתמש של המחבר ותוכן כלשהו:

‎/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

כללי המשחק

עכשיו, כשתפקידי המשתמשים מתועדים במסד הנתונים, צריך לכתוב כללי אבטחה כדי לאמת אותם. הכללים האלה מניחים שהאפליקציה משתמשת ב-Firebase Auth, כך שהמשתנה request.auth.uid הוא מזהה המשתמש.

שלב 1: מתחילים עם קובץ כללים בסיסי, שכולל כללים ריקים לסטוריז ולתגובות:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

שלב 2: מוסיפים כלל write פשוט שנותן לבעלים שליטה מלאה בסטוריז. הפונקציות שמוגדרות עוזרות לקבוע את התפקידים של המשתמשים ואם מסמכים חדשים תקינים:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          // Read from the "roles" map in the resource (rsc).
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          // Determine if the user is one of an array of roles
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          // Valid if story does not exist and the new story has the correct owner.
          return resource == null && isOneOfRoles(request.resource, ['owner']);
        }

        // Owners can read, write, and delete stories
        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

         match /comments/{comment} {
            // ...
         }
     }
   }
}

שלב 3: כותבים כללים שמאפשרים למשתמשים בכל תפקיד לקרוא סיפורים ותגובות. השימוש בפונקציות שהוגדרו בשלב הקודם שומר על הכללים תמציתיים וקריאים:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

        // Any role can read stories.
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          // Any role can read comments.
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
        }
     }
   }
}

שלב 4: מאפשרים לכותבי הסיפורים, למגיבים ולבעלים לפרסם תגובות. שימו לב שהכלל הזה גם מאמת שהשדה owner של התגובה תואם למשתמש ששלח את הבקשה, וכך מונע ממשתמשים לכתוב על תגובות של משתמשים אחרים:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);

          // Owners, writers, and commenters can create comments. The
          // user id in the comment document must match the requesting
          // user's id.
          //
          // Note: we have to use get() here to retrieve the story
          // document so that we can check the user's role.
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

שלב 5: נותנים לכותבים את היכולת לערוך את תוכן הכתבה, אבל לא לערוך את התפקידים בכתבה או לשנות מאפיינים אחרים של המסמך. כדי לעשות את זה, צריך לפצל את הכלל write של stories לכללים נפרדים ל-create, ל-update ול-delete, כי כותבים יכולים רק לעדכן סיפורים:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return request.resource.data.roles[request.auth.uid] == 'owner';
        }

        function onlyContentChanged() {
          // Ensure that title and roles are unchanged and that no new
          // fields are added to the document.
          return request.resource.data.title == resource.data.title
            && request.resource.data.roles == resource.data.roles
            && request.resource.data.keys() == resource.data.keys();
        }

        // Split writing into creation, deletion, and updating. Only an
        // owner can create or delete a story but a writer can update
        // story content.
        allow create: if isValidNewStory();
        allow delete: if isOneOfRoles(resource, ['owner']);
        allow update: if isOneOfRoles(resource, ['owner'])
                      || (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

מגבלות

הפתרון שמוצג למעלה מדגים אבטחה של נתוני משתמשים באמצעות כללי אבטחה, אבל חשוב להיות מודעים למגבלות הבאות:

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