許多協作應用程式會根據一組權限,允許使用者讀取及寫入不同的資料。舉例來說,在文件編輯應用程式中,使用者可能希望允許少數使用者讀取及寫入文件,同時封鎖不必要的存取權。
解決方案:角色型存取權控管
您可以運用 Cloud Firestore 的資料模型和自訂安全性規則,在應用程式中實作角色型存取控制。
假設您要建構協作撰寫應用程式,使用者可建立「故事」和「留言」,並符合下列安全性需求:
- 每則故事都有一位擁有者,並可與「撰寫者」、「加註者」和「讀者」共用。
- 讀者只能查看故事和留言,但無法編輯任何內容。
- 加註者擁有讀者的所有存取權,也可以在故事中新增註解。
- 撰寫者擁有留言者的所有存取權,也可以編輯故事內容。
- 擁有者可以編輯故事的任何部分,也可以控管其他使用者的存取權。
資料結構
假設您的應用程式有 stories 集合,其中每個文件都代表一個故事。每個故事也有 comments 子集合,其中每個文件都是該故事的留言。
如要追蹤存取角色,請新增 roles 欄位,這是使用者 ID 對應角色的對應:
/stories/{storyid}
{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}
留言只包含兩個欄位:作者的使用者 ID 和部分內容:
/stories/{storyid}/comments/{commentid}
{
  user: "alice",
  content: "I think this is a great story!"
}
規則
現在您已在資料庫中記錄使用者的角色,接下來需要撰寫安全規則來驗證這些角色。這些規則假設應用程式使用 Firebase Auth,因此 request.auth.uid
變數是使用者 ID。
步驟 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:授予撰稿人編輯故事內容的權限,但不得編輯故事角色或變更文件的任何其他屬性。因此,您必須將 stories 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;
        }
     }
   }
}
限制
上述解決方案示範如何使用安全規則保護使用者資料,但請注意下列限制:
- 精細程度:在上述範例中,多個角色 (撰寫者和擁有者) 具有相同文件的寫入權限,但限制不同。如果文件較為複雜,管理起來可能會很困難,因此建議將單一文件分割成多份文件,每份文件都由單一角色擁有。
- 大型群組:如要與非常大型或複雜的群組共用,請考慮使用系統,將角色儲存在自己的集合中,而不是儲存在目標文件的欄位中。