許多協作應用程式允許使用者根據一組權限讀取和寫入不同的資料。例如,在文檔編輯應用程式中,使用者可能希望允許少數使用者讀取和寫入其文檔,同時阻止不必要的存取。
解決方案:基於角色的存取控制
您可以利用 Cloud Firestore 的資料模型以及自訂安全規則在應用程式中實現基於角色的存取控制。
假設您正在建立一個協作寫作應用程序,用戶可以在其中創建具有以下安全要求的“故事”和“評論”:
- 每個故事都有一個所有者,可以與“作者”、“評論者”和“讀者”分享。
- 讀者只能看到故事和評論。他們無法編輯任何內容。
- 評論者擁有讀者的所有訪問權限,他們還可以為故事添加評論。
- 作者擁有評論者的所有訪問權限,他們還可以編輯故事內容。
- 擁有者可以編輯故事的任何部分以及控制其他使用者的存取權限。
資料結構
假設您的應用程式有一個stories
集合,其中每個文件代表一個故事。每個故事還有一個comments
子集合,其中每個文件都是對該故事的評論。
若要追蹤存取角色,請新增roles
字段,該字段是使用者 ID 到角色的映射:
/故事/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
評論只包含兩個字段,作者的用戶 ID 和一些內容:
/故事/{storyid}/評論/{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 :讓作者能夠編輯故事內容,但不能編輯故事角色或更改文件的任何其他屬性。這需要將故事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;
}
}
}
}
限制
上面顯示的解決方案演示了使用安全規則保護用戶數據,但您應該注意以下限制:
- 粒度:在上面的範例中,多個角色(作者和擁有者)對相同文件具有寫入存取權限,但具有不同的限制。對於更複雜的文檔來說,這可能會變得難以管理,並且最好將單個文檔拆分為多個文檔,每個文檔由一個角色擁有。
- 大型群組:如果您需要與非常大或複雜的群組共享,請考慮將角色儲存在其自己的集合中而不是作為目標文件上的欄位的系統。