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 các phần dữ liệu khác nhau dựa trên một bộ 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 số người dùng đọc và ghi tài liệu của họ trong khi chặn quyề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 tuỳ chỉnh để triển khai tính năng kiểm soát quyền truy cập dựa trên vai trò trong ứng dụng của mình.

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

  • Mỗi câu chuyện đều 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".
  • Người đọc chỉ có thể xem câu chuyện 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ó tất cả quyền truy cập của người đọc, đồng thời có thể thêm bình luận vào một câu chuyện.
  • Người viết có tất cả quyền truy cập của người bình luận, đồng thời có thể chỉnh sửa nội dung câu chuyện.
  • Chủ sở hữu có thể chỉnh sửa mọi phần 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 tập hợp stories, trong đó mỗi tài liệu đại diện cho 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à một bình luận về câu chuyện đó.

Để theo dõi các vai trò truy cập, hãy thêm một trường roles là bản đồ mã nhận dạng người dùng cho các vai trò:

/stories/{storyid}

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

Bình luận chỉ chứa 2 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}/comments/{commentid}

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

Luật chơi

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

Bước 1: Bắt đầu bằng một tệp quy tắc cơ bản, bao gồm các quy tắc trống cho tin và bình luận:

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 để cấp cho chủ sở hữu toàn quyền kiểm soát tin. Các hàm được xác định sẽ giúp xác định vai trò của người dùng và liệu các tài liệu mới có hợp lệ hay không:

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 truyện và bình luận. Việc sử dụng các hàm được xác định ở bước trước giúp 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 truyện, người bình luận và chủ sở hữu đăng bình luận. Xin lưu ý rằng quy tắc này cũng xác thực rằng owner của bình luận khớp với người dùng yêu cầu, điều này ngăn người dùng ghi đè bình luận 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: Cho phép người viết chỉnh sửa nội dung câu chuyện, nhưng không được chỉnh sửa vai trò trong câu chuyện hoặc thay đổi bất kỳ thuộc tính nào khác của tài liệu. Điều này đòi hỏi bạn phải chia quy tắc write về 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âu chuyện:

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ạ cách bảo mật dữ liệu người dùng bằng cách sử dụng Quy tắc bảo mật, nhưng bạn cần lưu ý những hạn chế sau:

  • Mức độ chi tiết: Trong ví dụ trên, nhiều vai trò (người viết và chủ sở hữu) có quyền ghi vào cùng một tài liệu nhưng có các hạn chế khác nhau. Việc này có thể trở nên khó quản lý hơn với các tài liệu phức tạp hơn và bạn nên chia tài liệu thành nhiều tài liệu, mỗi tài liệu do một vai trò duy nhất sở hữu.
  • 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 cân nhắc một hệ thống lưu trữ vai trò trong bộ sưu tập riêng thay vì dưới dạng một trường trên tài liệu đích.