Quyền truy cập của người dùng và nhóm vào dữ liệu một cách an toàn

Nhiều ứng dụng cộng tác cho phép người dùng đọc và ghi nhiều phần dữ liệu dựa trên một nhóm quyền. Ví dụ: trong một ứng dụng chỉnh sửa tài liệu: người dùng có thể muốn cho phép một vài người dùng đọc và viết tài liệu của họ trong khi chặn truy cập không mong muốn.

Giải pháp: Kiểm soát quyền truy cập dựa trên vai trò

Bạn có thể tận dụng mô hình dữ liệu của Cloud Firestore cũng như các quy tắc bảo mật để triển khai quyền truy cập dựa trên vai trò kiểm soát trong ứng dụng của bạn.

Giả sử bạn đang xây dựng ứng dụng viết cộng tác trong đó người dùng có thể tạo "tin bài" và "bình luận" bằng các yêu cầu về bảo mật như sau:

  • Mỗi câu chuyện có một chủ sở hữu và có thể được chia sẻ với "người viết", "người nhận xét" và "người đọc".
  • Độc giả chỉ có thể xem tin bài và bình luận. Họ không thể chỉnh sửa bất kỳ nội dung nào.
  • Người nhận xét có toàn quyền truy cập của độc giả và họ cũng có thể thêm bình luận vào tin bài.
  • Người viết có toàn quyền truy cập như người bình luận và họ cũng có thể chỉnh sửa nội dung câu chuyện.
  • Chủ sở hữu có thể chỉnh sửa bất kỳ phần nào của câu chuyện, cũng như kiểm soát quyền truy cập của người dùng khác.

Cấu trúc dữ liệu

Giả sử ứng dụng của bạn có một bộ sưu tập stories, trong đó mỗi tài liệu biểu thị một câu chuyện. Mỗi câu chuyện cũng có một bộ sưu tập con comments, trong đó mỗi tài liệu là nhận xét về câu chuyện đó.

Để theo dõi các vai trò truy cập, hãy thêm trường roles là bản đồ của user ID sang vai trò:

/Stories/{storyid}

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

Nhận xét chỉ chứa hai trường, mã nhận dạng người dùng của tác giả và một số nội dung:

/Stories/{storyid}/bình luận/{commentid}

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

Quy tắc

Giờ đây, khi bạn đã có người dùng vai trò được ghi lại trong cơ sở dữ liệu, bạn cần ghi Quy tắc bảo mật để xác thực các quy tắc đó. Những quy tắc này giả định ứng dụng sử dụng Xác thực Firebase để request.auth.uid biến là mã nhận dạng của người dùng.

Bước 1: Bắt đầu với tệp quy tắc cơ bản, trong đó có các quy tắc trống cho story và nhận xét:

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

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

Bước 2: Thêm một quy tắc write đơn giản để chủ sở hữu có toàn quyền kiểm soát câu chuyện của bạn. Các hàm được định nghĩa giúp xác định vai trò của người dùng và liệu người dùng mới có giấy tờ hợp lệ:

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} {
            // ...
         }
     }
   }
}

Bước 3: Viết các quy tắc cho phép người dùng có vai trò bất kỳ đọc tin bài và nhận xét. Việc sử dụng các hàm được xác định ở bước trước giúp đảm bảo các quy tắc ngắn gọn và dễ đọc:

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']);
        }
     }
   }
}

Bước 4: Cho phép người viết câu chuyện, người nhận xét và chủ sở hữu đăng nhận xét. Lưu ý rằng quy tắc này cũng xác thực rằng owner của nhận xét khớp với yêu cầu người dùng, khiến người dùng không thể viết lên nhận xét của nhau:

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

Bước 5: Cấp cho người viết quyền chỉnh sửa nội dung câu chuyện nhưng không được chỉnh sửa câu chuyện vai trò hoặc thay đổi bất kỳ thuộc tính nào khác của tài liệu. Bạn cần chia tách quy tắc write câu chuyện thành các quy tắc riêng biệt cho create, updatedelete vì người viết chỉ có thể cập nhật các tin bài:

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

Các điểm hạn chế

Giải pháp nêu trên minh hoạ việc bảo mật dữ liệu người dùng bằng các Quy tắc bảo mật, nhưng bạn cũng cần lưu ý những hạn chế sau:

  • Mức độ chi tiết: Trong ví dụ trên, có nhiều vai trò (nhà văn và chủ sở hữu) có quyền ghi vào cùng một tài liệu nhưng có các giới hạn khác nhau. Khi sử dụng các tài liệu phức tạp hơn, bạn có thể gặp khó khăn khi quản lý việc này và tốt hơn là nên chia từng tài liệu thành nhiều tài liệu, mỗi tài liệu thuộc sở hữu của một vai trò.
  • Nhóm lớn: Nếu bạn cần chia sẻ với các nhóm rất lớn hoặc phức tạp, hãy xem xét một hệ thống trong đó vai trò được lưu trữ trong tập hợp riêng của chúng thay vì thay vì dưới dạng một trường trên tài liệu đích.