1. ก่อนเริ่มต้น
Cloud Firestore, Cloud Storage สำหรับ Firebase และฐานข้อมูลเรียลไทม์จะใช้ไฟล์การกำหนดค่าที่คุณเขียนเพื่อให้สิทธิ์การอ่านและการเขียน การกำหนดค่าดังกล่าวที่เรียกว่ากฎความปลอดภัยยังทำหน้าที่เป็นสคีมาประเภทหนึ่งสำหรับแอปของคุณได้ด้วย และเป็นส่วนที่สำคัญที่สุดส่วนหนึ่งในการพัฒนาแอปพลิเคชัน และ Codelab นี้จะแนะนำวิธีการให้คุณ
ข้อกำหนดเบื้องต้น
- โปรแกรมแก้ไขอย่างง่าย เช่น Visual Studio Code, Atom หรือ Sublime Text
- Node.js 8.6.0 ขึ้นไป (หากต้องการติดตั้ง Node.js ให้ใช้ nvm หากต้องการตรวจสอบเวอร์ชัน ให้เรียกใช้
node --version
) - Java 7 ขึ้นไป (หากต้องการติดตั้ง Java ให้ใช้คำสั่งเหล่านี้ หากต้องการตรวจสอบเวอร์ชัน ให้เรียกใช้
java -version
)
สิ่งที่คุณต้องทำ
ในโค้ดแล็บนี้ คุณจะได้รักษาความปลอดภัยของแพลตฟอร์มบล็อกอย่างง่ายที่สร้างขึ้นบน Firestore คุณจะใช้โปรแกรมจำลอง Firestore เพื่อเรียกใช้การทดสอบหน่วยกับกฎการรักษาความปลอดภัย และตรวจสอบว่ากฎอนุญาตและไม่อนุญาตการเข้าถึงที่คุณคาดหวัง
โดยคุณจะได้เรียนรู้วิธีต่อไปนี้
- ให้สิทธิ์แบบละเอียด
- บังคับใช้การตรวจสอบข้อมูลและประเภท
- ใช้การควบคุมการเข้าถึงตามแอตทริบิวต์
- ให้สิทธิ์เข้าถึงตามวิธีการตรวจสอบสิทธิ์
- สร้างฟังก์ชันที่กำหนดเอง
- สร้างกฎความปลอดภัยตามเวลา
- ใช้รายการที่ไม่อนุญาตและลบแบบไม่ถาวร
- ทําความเข้าใจเวลาที่จะยกเลิกการทําให้ข้อมูลเป็นปกติเพื่อให้ตรงกับรูปแบบการเข้าถึงหลายรูปแบบ
2. ตั้งค่า
นี่คือแอปพลิเคชันบล็อก สรุปภาพรวมระดับสูงของฟังก์ชันการทำงานของแอปพลิเคชันมีดังนี้
ร่างบล็อกโพสต์:
- ผู้ใช้สามารถสร้างฉบับร่างของบล็อกโพสต์ ซึ่งจะอยู่ในคอลเล็กชัน
drafts
- ผู้เขียนจะอัปเดตฉบับร่างต่อไปได้จนกว่าจะพร้อมเผยแพร่
- เมื่อพร้อมเผยแพร่ ระบบจะทริกเกอร์ฟังก์ชัน Firebase ที่สร้างเอกสารใหม่ในคอลเล็กชัน
published
- ผู้เขียนหรือผู้ดูแลเว็บไซต์สามารถลบฉบับร่างได้
บล็อกโพสต์ที่เผยแพร่:
- ผู้ใช้จะสร้างโพสต์ที่เผยแพร่แล้วไม่ได้ แต่จะสร้างได้ผ่านฟังก์ชันเท่านั้น
- คุณทำได้แค่ลบแบบไม่ถาวร ซึ่งจะอัปเดตแอตทริบิวต์
visible
เป็น false
ความคิดเห็น
- โพสต์ที่เผยแพร่แล้วจะอนุญาตให้แสดงความคิดเห็น ซึ่งเป็นคอลเล็กชันย่อยในแต่ละโพสต์ที่เผยแพร่
- ผู้ใช้ต้องมีอีเมลที่ยืนยันแล้วและไม่อยู่ในรายชื่อผู้ปฏิเสธจึงจะแสดงความคิดเห็นได้ เพื่อลดการละเมิด
- คุณจะอัปเดตความคิดเห็นได้ภายใน 1 ชั่วโมงหลังจากที่โพสต์เท่านั้น
- ผู้เขียนความคิดเห็น ผู้เขียนโพสต์ต้นฉบับ หรือผู้ดูแลสามารถลบความคิดเห็นได้
นอกจากกฎการเข้าถึงแล้ว คุณยังต้องสร้างกฎความปลอดภัยที่บังคับใช้ช่องที่จำเป็นและการตรวจสอบข้อมูลด้วย
ทุกอย่างจะเกิดขึ้นในเครื่องโดยใช้ Firebase Emulator Suite
ดูซอร์สโค้ด
ในโค้ดแล็บนี้ คุณจะเริ่มต้นด้วยการทดสอบกฎความปลอดภัย แต่มีกฎความปลอดภัยน้อยที่สุด ดังนั้นสิ่งแรกที่คุณต้องทำคือโคลนแหล่งที่มาเพื่อเรียกใช้การทดสอบ
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
จากนั้นให้ย้ายไปที่ไดเรกทอรี initial-state ซึ่งคุณจะใช้ทำงานใน Codelab นี้ต่อไป
$ cd codelab-rules/initial-state
ตอนนี้ให้ติดตั้งการอ้างอิงเพื่อให้คุณเรียกใช้การทดสอบได้ หากใช้การเชื่อมต่ออินเทอร์เน็ตที่ช้า การดำเนินการนี้อาจใช้เวลา 1-2 นาที
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
รับ Firebase CLI
ชุดโปรแกรมจำลองที่คุณจะใช้เรียกใช้การทดสอบเป็นส่วนหนึ่งของ Firebase CLI (อินเทอร์เฟซบรรทัดคำสั่ง) ซึ่งติดตั้งในเครื่องได้ด้วยคำสั่งต่อไปนี้
$ npm install -g firebase-tools
จากนั้นตรวจสอบว่าคุณใช้ CLI เวอร์ชันล่าสุด Codelab นี้ควรใช้ได้กับเวอร์ชัน 8.4.0 ขึ้นไป แต่เวอร์ชันที่ใหม่กว่าจะมีการแก้ไขข้อบกพร่องเพิ่มเติม
$ firebase --version 9.10.2
3. ทำการทดสอบ
ในส่วนนี้ คุณจะเรียกใช้การทดสอบในเครื่อง ซึ่งหมายความว่าถึงเวลาบูตชุดโปรแกรมจำลองแล้ว
เริ่มโปรแกรมจำลอง
แอปพลิเคชันที่คุณจะใช้มีคอลเล็กชัน Firestore หลัก 3 รายการ ได้แก่ drafts
ซึ่งมีโพสต์ในบล็อกที่อยู่ระหว่างดำเนินการ คอลเล็กชัน published
ซึ่งมีโพสต์ในบล็อกที่เผยแพร่แล้ว และ comments
ซึ่งเป็นคอลเล็กชันย่อยในโพสต์ที่เผยแพร่แล้ว ที่เก็บมาพร้อมกับการทดสอบหน่วยสำหรับกฎความปลอดภัยที่กำหนดแอตทริบิวต์ของผู้ใช้และเงื่อนไขอื่นๆ ที่จำเป็นเพื่อให้ผู้ใช้สร้าง อ่าน อัปเดต และลบเอกสารในคอลเล็กชัน drafts
, published
และ comments
ได้ คุณจะต้องเขียนกฎความปลอดภัยเพื่อให้การทดสอบเหล่านั้นผ่าน
โดยเริ่มจากฐานข้อมูลจะถูกล็อก การอ่านและการเขียนไปยังฐานข้อมูลจะถูกปฏิเสธทั้งหมด และการทดสอบทั้งหมดจะล้มเหลว เมื่อคุณเขียนกฎความปลอดภัย การทดสอบจะผ่าน หากต้องการดูการทดสอบ ให้เปิด functions/test.js
ในเอดิเตอร์
ในบรรทัดคำสั่ง ให้เริ่มโปรแกรมจำลองโดยใช้ emulators:exec
แล้วเรียกใช้การทดสอบ
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
เลื่อนไปที่ด้านบนสุดของเอาต์พุต
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
ตอนนี้มีข้อผิดพลาด 9 รายการ ขณะสร้างไฟล์กฎ คุณสามารถวัดความคืบหน้าได้โดยดูว่าการทดสอบผ่านมากขึ้น
4. สร้างฉบับร่างของบล็อกโพสต์
เนื่องจากสิทธิ์เข้าถึงบล็อกโพสต์ฉบับร่างแตกต่างจากสิทธิ์เข้าถึงบล็อกโพสต์ที่เผยแพร่แล้ว แอปเขียนบล็อกนี้จึงจัดเก็บบล็อกโพสต์ฉบับร่างไว้ในคอลเล็กชันแยกต่างหาก /drafts
โดยมีเพียงผู้เขียนหรือผู้ดูแลเท่านั้นที่จะเข้าถึงฉบับร่างได้ และมีการตรวจสอบความถูกต้องสำหรับฟิลด์ที่จำเป็นและแก้ไขไม่ได้
เมื่อเปิดไฟล์ firestore.rules
คุณจะเห็นไฟล์กฎเริ่มต้น
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
คำสั่งที่ตรงกัน match /{document=**}
ใช้ไวยากรณ์ **
เพื่อใช้กับเอกสารทั้งหมดในคอลเล็กชันย่อยแบบเรียกซ้ำ และเนื่องจากเป็นกฎระดับบนสุด กฎเดียวกันนี้จึงมีผลกับคำขอทั้งหมดในตอนนี้ ไม่ว่าใครจะเป็นผู้ส่งคำขอหรือพยายามอ่านหรือเขียนข้อมูลใด
เริ่มโดยนำคำสั่งจับคู่ที่อยู่ด้านในสุดออก แล้วแทนที่ด้วย match /drafts/{draftID}
(ความคิดเห็นเกี่ยวกับโครงสร้างของเอกสารอาจมีประโยชน์ในกฎ และจะรวมไว้ใน Codelab นี้ โดยความคิดเห็นจะเป็นตัวเลือกเสมอ)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
กฎแรกที่คุณจะเขียนสำหรับฉบับร่างจะควบคุมผู้ที่สร้างเอกสารได้ ในแอปพลิเคชันนี้ เฉพาะบุคคลที่ระบุเป็นผู้เขียนเท่านั้นที่จะสร้างฉบับร่างได้ ตรวจสอบว่า UID ของผู้ที่ส่งคำขอเป็น UID เดียวกันกับที่ระบุไว้ในเอกสาร
เงื่อนไขแรกสำหรับการสร้างจะเป็นดังนี้
request.resource.data.authorUID == request.auth.uid
ถัดมา คุณจะสร้างเอกสารได้ก็ต่อเมื่อมีช่องที่จำเป็นทั้ง 3 ช่อง ได้แก่ authorUID
, createdAt
และ title
(ผู้ใช้ไม่ได้ระบุฟิลด์ createdAt
แต่เป็นการบังคับให้แอปต้องเพิ่มฟิลด์นี้ก่อนที่จะพยายามสร้างเอกสาร) เนื่องจากคุณเพียงแค่ต้องตรวจสอบว่ามีการสร้างแอตทริบิวต์ คุณจึงตรวจสอบได้ว่า request.resource
มีคีย์ทั้งหมดเหล่านั้น
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
ข้อกำหนดสุดท้ายสำหรับการสร้างบล็อกโพสต์คือชื่อต้องมีความยาวไม่เกิน 50 อักขระ
request.resource.data.title.size() < 50
เนื่องจากเงื่อนไขทั้งหมดนี้ต้องเป็นจริง ให้เชื่อมเงื่อนไขเหล่านี้เข้าด้วยกันโดยใช้โอเปอเรเตอร์ AND เชิงตรรกะ &&
กฎแรกจึงกลายเป็น
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
ในเทอร์มินัล ให้เรียกใช้การทดสอบอีกครั้งและยืนยันว่าการทดสอบแรกผ่าน
5. อัปเดตฉบับร่างของบล็อกโพสต์
จากนั้นเมื่อผู้เขียนปรับแต่งบล็อกโพสต์ฉบับร่าง ก็จะแก้ไขเอกสารฉบับร่าง สร้างกฎสำหรับเงื่อนไขเมื่ออัปเดตโพสต์ได้ ก่อนอื่น มีเพียงผู้เขียนเท่านั้นที่อัปเดตฉบับร่างของตนเองได้ โปรดทราบว่าในส่วนนี้คุณจะตรวจสอบ UID ที่เขียนไว้แล้วresource.data.authorUID
:
resource.data.authorUID == request.auth.uid
ข้อกำหนดที่ 2 สำหรับการอัปเดตคือแอตทริบิวต์ 2 รายการ ได้แก่ authorUID
และ createdAt
ไม่ควรเปลี่ยนแปลง
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
และสุดท้าย ชื่อควรมีความยาวไม่เกิน 50 อักขระ
request.resource.data.title.size() < 50;
เนื่องจากต้องมีคุณสมบัติตรงตามเงื่อนไขทั้งหมดนี้ ให้ต่อเงื่อนไขเข้าด้วยกันโดยใช้ &&
ดังนี้
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
กฎฉบับเต็มจะกลายเป็น
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
เรียกใช้การทดสอบอีกครั้งและยืนยันว่าการทดสอบอื่นผ่าน
6. ลบและอ่านฉบับร่าง: การควบคุมการเข้าถึงตามแอตทริบิวต์
ผู้เขียนสามารถสร้างและอัปเดตฉบับร่างได้เช่นเดียวกับการลบฉบับร่าง
resource.data.authorUID == request.auth.uid
นอกจากนี้ ผู้เขียนที่มีแอตทริบิวต์ isModerator
ในโทเค็นการตรวจสอบสิทธิ์จะได้รับอนุญาตให้ลบฉบับร่างได้
request.auth.token.isModerator == true
เนื่องจากเงื่อนไขใดเงื่อนไขหนึ่งก็เพียงพอสำหรับการลบแล้ว ให้เชื่อมเงื่อนไขเข้าด้วยกันด้วยโอเปอเรเตอร์ OR ทางตรรกะ ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
เงื่อนไขเดียวกันนี้ใช้กับการอ่านด้วย เพื่อให้เพิ่มสิทธิ์ลงในกฎได้
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
ตอนนี้กฎฉบับเต็มมีดังนี้
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
เรียกใช้การทดสอบอีกครั้งและยืนยันว่าตอนนี้การทดสอบอื่นผ่านแล้ว
7. อ่าน สร้าง และลบโพสต์ที่เผยแพร่แล้ว: การลดรูปแบบปกติสำหรับรูปแบบการเข้าถึงที่แตกต่างกัน
เนื่องจากรูปแบบการเข้าถึงโพสต์ที่เผยแพร่แล้วและโพสต์ฉบับร่างแตกต่างกันมาก แอปนี้จึงยกเลิกการทำให้เป็นมาตรฐานของโพสต์ลงในคอลเล็กชัน draft
และ published
แยกกัน เช่น ทุกคนอ่านโพสต์ที่เผยแพร่แล้วได้ แต่จะลบจริงไม่ได้ ส่วนฉบับร่างจะลบได้ แต่จะมีเพียงผู้เขียนและผู้ดูแลเท่านั้นที่อ่านได้ ในแอปนี้ เมื่อผู้ใช้ต้องการเผยแพร่บล็อกโพสต์ฉบับร่าง ระบบจะทริกเกอร์ฟังก์ชันที่จะสร้างโพสต์ใหม่ที่เผยแพร่แล้ว
จากนั้นคุณจะเขียนกฎสำหรับโพสต์ที่เผยแพร่ กฎที่เขียนง่ายที่สุดคือทุกคนสามารถอ่านโพสต์ที่เผยแพร่ได้ แต่ไม่มีใครสร้างหรือลบโพสต์ได้ เพิ่มกฎต่อไปนี้
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
เมื่อเพิ่มรายการเหล่านี้ลงในกฎที่มีอยู่ ไฟล์กฎทั้งหมดจะเป็นดังนี้
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
เรียกใช้การทดสอบอีกครั้ง และยืนยันว่าการทดสอบอื่นผ่าน
8. การอัปเดตโพสต์ที่เผยแพร่: ฟังก์ชันที่กำหนดเองและตัวแปรภายใน
เงื่อนไขในการอัปเดตโพสต์ที่เผยแพร่แล้วมีดังนี้
- โดยผู้เขียนหรือผู้ดูแลเท่านั้นที่ดำเนินการนี้ได้ และ
- ต้องมีช่องที่ต้องกรอกทั้งหมด
เนื่องจากคุณได้เขียนเงื่อนไขในการเป็นผู้เขียนหรือผู้ดูแลระบบไว้แล้ว คุณจึงคัดลอกและวางเงื่อนไขได้ แต่เมื่อเวลาผ่านไป เงื่อนไขดังกล่าวอาจอ่านและดูแลรักษายาก แต่คุณจะสร้างฟังก์ชันที่กำหนดเองซึ่งห่อหุ้มตรรกะสำหรับการเป็นผู้เขียนหรือผู้ดูแล จากนั้นคุณจะเรียกใช้จากหลายเงื่อนไข
สร้างฟังก์ชันที่กำหนดเอง
สร้างฟังก์ชันใหม่ชื่อ isAuthorOrModerator
เหนือคำสั่งจับคู่สำหรับฉบับร่าง โดยรับเอกสารโพสต์ (ใช้ได้กับทั้งฉบับร่างและโพสต์ที่เผยแพร่แล้ว) และออบเจ็กต์การให้สิทธิ์ของผู้ใช้เป็นอาร์กิวเมนต์
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
ใช้ตัวแปรภายใน
ภายในฟังก์ชัน ให้ใช้คีย์เวิร์ด let
เพื่อตั้งค่าตัวแปร isAuthor
และ isModerator
ฟังก์ชันทั้งหมดต้องลงท้ายด้วยคำสั่ง return และฟังก์ชันของเราจะแสดงผลบูลีนที่ระบุว่าตัวแปรใดตัวแปรหนึ่งเป็นจริงหรือไม่
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
เรียกใช้ฟังก์ชัน
ตอนนี้คุณจะอัปเดตกฎสำหรับฉบับร่างเพื่อเรียกใช้ฟังก์ชันดังกล่าว โดยระมัดระวังในการส่ง resource.data
เป็นอาร์กิวเมนต์แรก
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
ตอนนี้คุณสามารถเขียนเงื่อนไขสำหรับการอัปเดตโพสต์ที่เผยแพร่ซึ่งใช้ฟังก์ชันใหม่ได้แล้ว โดยทำดังนี้
allow update: if isAuthorOrModerator(resource.data, request.auth);
เพิ่มการตรวจสอบ
ไม่ควรเปลี่ยนแปลงฟิลด์บางรายการของโพสต์ที่เผยแพร่แล้ว โดยเฉพาะฟิลด์ url
, authorUID
และ publishedAt
จะเปลี่ยนแปลงไม่ได้ ฟิลด์อีก 2 รายการ ได้แก่ title
และ content
รวมถึง visible
จะยังคงอยู่หลังการอัปเดต เพิ่มเงื่อนไขเพื่อบังคับใช้ข้อกำหนดต่อไปนี้สำหรับการอัปเดตโพสต์ที่เผยแพร่
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
สร้างฟังก์ชันที่กำหนดเองด้วยตนเอง
และสุดท้าย ให้เพิ่มเงื่อนไขว่าชื่อต้องมีความยาวไม่เกิน 50 อักขระ เนื่องจากนี่คือตรรกะที่นำกลับมาใช้ใหม่ คุณจึงทำได้โดยการสร้างฟังก์ชันใหม่ titleIsUnder50Chars
ฟังก์ชันใหม่นี้จะทำให้เงื่อนไขในการอัปเดตโพสต์ที่เผยแพร่แล้วเป็นดังนี้
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
และไฟล์กฎที่สมบูรณ์คือ
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
เรียกใช้การทดสอบอีกครั้ง ตอนนี้คุณควรมีการทดสอบที่ผ่าน 5 รายการและการทดสอบที่ไม่ผ่าน 4 รายการ
9. ความคิดเห็น: สิทธิ์ของผู้ให้บริการคอลเล็กชันย่อยและผู้ให้บริการลงชื่อเข้าใช้
โพสต์ที่เผยแพร่จะอนุญาตให้แสดงความคิดเห็นได้ และระบบจะจัดเก็บความคิดเห็นไว้ในคอลเล็กชันย่อยของโพสต์ที่เผยแพร่ (/published/{postID}/comments/{commentID}
) โดยค่าเริ่มต้น กฎของคอลเล็กชันจะไม่มีผลกับคอลเล็กชันย่อย คุณไม่ต้องการให้กฎเดียวกันที่ใช้กับเอกสารหลักของโพสต์ที่เผยแพร่มีผลกับความคิดเห็น คุณจึงต้องสร้างกฎที่แตกต่างกัน
หากต้องการเขียนกฎสำหรับการเข้าถึงความคิดเห็น ให้เริ่มต้นด้วยคำสั่ง match ดังนี้
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
การอ่านความคิดเห็น: ต้องไม่ระบุตัวตน
สำหรับแอปนี้ เฉพาะผู้ใช้ที่สร้างบัญชีถาวรเท่านั้นที่จะอ่านความคิดเห็นได้ โดยผู้ใช้ที่ไม่ระบุชื่อจะอ่านไม่ได้ หากต้องการบังคับใช้กฎดังกล่าว ให้ค้นหาแอตทริบิวต์ sign_in_provider
ในออบเจ็กต์ auth.token
แต่ละรายการ
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
เรียกใช้การทดสอบอีกครั้ง และตรวจสอบว่าการทดสอบอีกรายการผ่าน
การสร้างความคิดเห็น: การตรวจสอบรายการที่ถูกปฏิเสธ
เงื่อนไขในการแสดงความคิดเห็นมี 3 ข้อดังนี้
- ผู้ใช้ต้องมีอีเมลที่ยืนยันแล้ว
- ความคิดเห็นต้องมีความยาวน้อยกว่า 500 อักขระ และ
- โดยผู้ใช้ดังกล่าวต้องไม่อยู่ในรายชื่อผู้ใช้ที่ถูกแบน ซึ่งจัดเก็บไว้ใน Firestore ในคอลเล็กชัน
bannedUsers
พิจารณาเงื่อนไขเหล่านี้ทีละข้อ
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
กฎสุดท้ายในการสร้างความคิดเห็นคือ
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
ตอนนี้ไฟล์กฎทั้งหมดมีลักษณะดังนี้
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
เรียกใช้การทดสอบอีกครั้ง และตรวจสอบว่าผ่านการทดสอบอีก 1 รายการ
10. การอัปเดตความคิดเห็น: กฎตามเวลา
ตรรกะทางธุรกิจสำหรับความคิดเห็นคือผู้เขียนความคิดเห็นจะแก้ไขความคิดเห็นได้เป็นเวลา 1 ชั่วโมงหลังจากสร้าง หากต้องการใช้ฟีเจอร์นี้ ให้ใช้createdAt
การประทับเวลา
ก่อนอื่น ให้ทำดังนี้เพื่อยืนยันว่าผู้ใช้เป็นผู้เขียน
request.auth.uid == resource.data.authorUID
จากนั้น ตรวจสอบว่าความคิดเห็นสร้างขึ้นภายในชั่วโมงที่ผ่านมา
(request.time - resource.data.createdAt) < duration.value(1, 'h');
เมื่อรวมเข้ากับโอเปอเรเตอร์ AND ทางตรรกะ กฎสำหรับการอัปเดตความคิดเห็นจะเป็นดังนี้
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
เรียกใช้การทดสอบอีกครั้ง และตรวจสอบว่าผ่านการทดสอบอีก 1 รายการ
11. การลบความคิดเห็น: ตรวจสอบการเป็นเจ้าของโดยผู้ปกครอง
ผู้เขียนความคิดเห็น ผู้ดูแล หรือผู้เขียนโพสต์ในบล็อกสามารถลบความคิดเห็นได้
ก่อนอื่น เนื่องจากฟังก์ชันตัวช่วยที่คุณเพิ่มไว้ก่อนหน้านี้จะตรวจสอบฟิลด์ authorUID
ที่อาจมีอยู่ในโพสต์หรือความคิดเห็น คุณจึงนำฟังก์ชันตัวช่วยกลับมาใช้ซ้ำเพื่อตรวจสอบว่าผู้ใช้เป็นผู้เขียนหรือผู้ดูแลได้หรือไม่
isAuthorOrModerator(resource.data, request.auth)
หากต้องการตรวจสอบว่าผู้ใช้เป็นผู้เขียนบล็อกโพสต์หรือไม่ ให้ใช้ get
เพื่อค้นหาโพสต์ใน Firestore
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
เนื่องจากเงื่อนไขใดเงื่อนไขหนึ่งก็เพียงพอแล้ว ให้ใช้โอเปอเรเตอร์ OR เชิงตรรกะระหว่างเงื่อนไขเหล่านั้น
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
เรียกใช้การทดสอบอีกครั้ง และตรวจสอบว่าผ่านการทดสอบอีก 1 รายการ
และไฟล์กฎทั้งหมดคือ
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. ขั้นตอนถัดไป
ยินดีด้วย คุณเขียนกฎความปลอดภัยที่ทำให้การทดสอบทั้งหมดผ่านและรักษาความปลอดภัยของแอปพลิเคชันแล้ว
หัวข้อที่เกี่ยวข้องซึ่งคุณควรศึกษาต่อมีดังนี้
- บล็อกโพสต์: วิธีตรวจสอบโค้ดของกฎความปลอดภัย
- Codelab: การพัฒนาแบบ Local First ด้วยโปรแกรมจำลอง
- วิดีโอ: วิธีตั้งค่า CI สำหรับการทดสอบที่ใช้โปรแกรมจำลองโดยใช้ GitHub Actions