รักษาความปลอดภัยการเข้าถึงข้อมูลสำหรับผู้ใช้และกลุ่ม

แอปที่ทำงานร่วมกันหลายแอปอนุญาตให้ผู้ใช้อ่านและเขียนข้อมูลต่างๆ ได้ตามชุดสิทธิ์ ตัวอย่างเช่น ในแอปแก้ไขเอกสาร ผู้ใช้อาจต้องการอนุญาตให้ผู้ใช้บางรายอ่านและเขียนเอกสารของตนได้ ขณะเดียวกันก็บล็อกการเข้าถึงที่ไม่พึงประสงค์

โซลูชัน: การควบคุมการเข้าถึงตามบทบาท

คุณสามารถใช้ประโยชน์จากโมเดลข้อมูลของ Cloud Firestore รวมถึงกฎความปลอดภัยที่กำหนดเองเพื่อใช้การควบคุมการเข้าถึงตามบทบาทในแอปได้

สมมติว่าคุณกำลังสร้างแอปพลิเคชันการเขียนร่วมกันซึ่งผู้ใช้สามารถสร้าง "เรื่องราว" และ "ความคิดเห็น" โดยมีข้อกำหนดด้านความปลอดภัยต่อไปนี้

  • เรื่องราวแต่ละรายการจะมีเจ้าของ 1 คน และสามารถแชร์กับ "ผู้เขียน" "ผู้แสดงความคิดเห็น" และ "ผู้อ่าน" ได้
  • ผู้อ่านจะดูได้เฉพาะเรื่องราวและความคิดเห็นเท่านั้น แต่จะแก้ไขไม่ได้
  • ผู้แสดงความคิดเห็นจะมีสิทธิ์เข้าถึงทั้งหมดของผู้อ่าน และยังเพิ่มความคิดเห็นในเรื่องราวได้ด้วย
  • นักเขียนมีสิทธิ์เข้าถึงทั้งหมดของผู้แสดงความคิดเห็น และยังแก้ไขเนื้อหาของเรื่องราวได้ด้วย
  • เจ้าของสามารถแก้ไขส่วนใดก็ได้ของเรื่องราว รวมถึงควบคุมสิทธิ์เข้าถึงของผู้ใช้รายอื่นๆ

โครงสร้างข้อมูล

สมมติว่าแอปของคุณมีคอลเล็กชัน stories ซึ่งเอกสารแต่ละรายการแสดงถึงเรื่องราว นอกจากนี้ แต่ละเรื่องราวยังมีคอลเล็กชันย่อย comments ซึ่งเอกสารแต่ละรายการเป็นความคิดเห็นเกี่ยวกับเรื่องราวนั้น

หากต้องการติดตามบทบาทการเข้าถึง ให้เพิ่มช่อง roles ซึ่งเป็นแผนที่ของรหัสผู้ใช้กับบทบาท ดังนี้

/stories/{storyid}

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

ความคิดเห็นมีเพียง 2 ช่อง ได้แก่ รหัสผู้ใช้ของผู้เขียนและเนื้อหาบางส่วน ดังนี้

/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;
        }
     }
   }
}

ข้อจำกัด

โซลูชันที่แสดงด้านบนแสดงให้เห็นถึงการรักษาความปลอดภัยข้อมูลผู้ใช้โดยใช้กฎความปลอดภัย แต่คุณควรทราบถึงข้อจำกัดต่อไปนี้

  • ความละเอียด: ในตัวอย่างข้างต้น บทบาทหลายบทบาท (ผู้เขียนและเจ้าของ) มีสิทธิ์เขียนในเอกสารเดียวกัน แต่มีข้อจำกัดที่แตกต่างกัน ซึ่งอาจทำให้จัดการได้ยากเมื่อเอกสารมีความซับซ้อนมากขึ้น และอาจเป็นการดีกว่าที่จะแยกเอกสารเดียวออกเป็นหลายเอกสาร โดยแต่ละเอกสารจะมีเจ้าของเพียงบทบาทเดียว
  • กลุ่มขนาดใหญ่: หากต้องการแชร์กับกลุ่มที่มีขนาดใหญ่หรือซับซ้อนมาก ให้พิจารณาระบบที่จัดเก็บบทบาทไว้ในคอลเล็กชันของตนเองแทนที่จะเป็นฟิลด์ในเอกสารเป้าหมาย