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