แอปการทำงานร่วมกันหลายแอปอนุญาตให้ผู้ใช้อ่านและเขียนข้อมูลส่วนต่างๆ ตามชุดสิทธิ์ ตัวอย่างเช่น ในแอปแก้ไขเอกสาร ผู้ใช้อาจต้องการอนุญาตให้ผู้ใช้เพียงไม่กี่คนอ่านและเขียนเอกสารของตนได้ขณะที่บล็อกการเข้าถึงที่ไม่พึงประสงค์
โซลูชัน: การควบคุมการเข้าถึงตามบทบาท
คุณใช้ประโยชน์จากรูปแบบข้อมูลของ Cloud Firestore รวมถึงกฎความปลอดภัยที่กำหนดเองเพื่อใช้การควบคุมการเข้าถึงตามบทบาทในแอปได้
สมมติว่าคุณกำลังสร้างแอปพลิเคชันการเขียนแบบร่วมมือกันซึ่งผู้ใช้สามารถสร้าง "เรื่องราว" และ "ความคิดเห็น" โดยมีข้อกำหนดด้านความปลอดภัยดังต่อไปนี้
- เรื่องราวแต่ละเรื่องมีเจ้าของเพียงคนเดียวและแชร์กับ "ผู้เขียน" "ผู้แสดงความคิดเห็น" และ "ผู้อ่าน" ได้
- ผู้อ่านจะดูได้เฉพาะเรื่องราวและความคิดเห็น แต่จะแก้ไขข้อมูลไม่ได้
- ผู้แสดงความคิดเห็นมีสิทธิ์เข้าถึงทั้งหมดเช่นเดียวกับผู้อ่าน และยังสามารถเพิ่มความคิดเห็นลงในเรื่องราวได้ด้วย
- ผู้เขียนมีสิทธิ์เข้าถึงทั้งหมดเช่นเดียวกับผู้แสดงความคิดเห็น และยังสามารถแก้ไขเนื้อหาของเรื่องราวได้ด้วย
- เจ้าของจะแก้ไขส่วนใดก็ได้ของเรื่องราว รวมถึงควบคุมการเข้าถึงของผู้ใช้คนอื่นๆ
โครงสร้างข้อมูล
สมมติว่าแอปของคุณมีคอลเล็กชัน stories
ที่เอกสารแต่ละรายการแสดงถึงเรื่องราว แต่ละเรื่องราวยังมีคอลเล็กชันย่อย comments
ที่เอกสารแต่ละรายการเป็นความคิดเห็นในเรื่องราวนั้นด้วย
หากต้องการตรวจสอบบทบาทการเข้าถึง ให้เพิ่มช่อง roles
ซึ่งเป็นการแมปรหัสผู้ใช้กับบทบาท ดังนี้
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
ความคิดเห็นมีเพียง 2 ช่อง ได้แก่ รหัสผู้ใช้ของผู้เขียนและเนื้อหาบางส่วน ดังนี้
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
กฎ
ในตอนนี้เมื่อคุณบันทึกบทบาทของผู้ใช้ไว้ในฐานข้อมูลแล้ว คุณจะต้องเขียนกฎความปลอดภัยเพื่อตรวจสอบความถูกต้องของบทบาทดังกล่าว กฎเหล่านี้จะถือว่าแอปใช้ Firebase Auth เพื่อให้ตัวแปร request.auth.uid
เป็นรหัสของผู้ใช้
ขั้นตอนที่ 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;
}
}
}
}
ข้อจำกัด
โซลูชันที่แสดงข้างต้นสาธิตการรักษาความปลอดภัยให้กับข้อมูลผู้ใช้โดยใช้กฎการรักษาความปลอดภัย แต่คุณควรทราบข้อจำกัดต่อไปนี้
- ความละเอียด: ในตัวอย่างข้างต้น บทบาทหลายบทบาท (ผู้เขียนและเจ้าของ) มีสิทธิ์เขียนในเอกสารเดียวกัน แต่มีข้อจำกัดแตกต่างกัน ซึ่งอาจทำให้จัดการได้ยากหากเอกสารมีความซับซ้อนมากขึ้น และคุณอาจต้องแยกเอกสารเดียวออกเป็นเอกสารหลายฉบับโดยให้แต่ละเอกสารมีเจ้าของเป็นบทบาทเดียว
- กลุ่มขนาดใหญ่: หากต้องการแชร์กับกลุ่มที่มีขนาดใหญ่หรือซับซ้อนมาก ให้พิจารณาใช้ระบบที่จัดเก็บบทบาทไว้ในคอลเล็กชันของตนเองแทนที่จะจัดเก็บเป็นช่องในเอกสารเป้าหมาย