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

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

פתרון: בקרת גישה מבוססת תפקידים

אתה יכול לנצל את מודל הנתונים של 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 הסיפורים לכללים נפרדים 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;
        }
     }
   }
}

מגבלות

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

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