1. ก่อนเริ่ม
Cloud Firestore, Cloud Storage for Firebase และ Realtime Database อาศัยไฟล์การกำหนดค่าที่คุณเขียนเพื่อให้สิทธิ์การเข้าถึงแบบอ่านและเขียน การกำหนดค่านั้นเรียกว่ากฎความปลอดภัย ยังสามารถทำหน้าที่เป็นสคีมาสำหรับแอปของคุณได้อีกด้วย เป็นหนึ่งในส่วนที่สำคัญที่สุดในการพัฒนาแอปพลิเคชันของคุณ และ Codelab นี้จะอธิบายให้คุณทราบ
ข้อกำหนดเบื้องต้น
- โปรแกรมแก้ไขอย่างง่าย เช่น Visual Studio Code, Atom หรือ Sublime Text
- Node.js 8.6.0 หรือสูงกว่า (ในการติดตั้ง Node.js ให้ใช้ nvm เพื่อตรวจสอบเวอร์ชันของคุณ เรียกใช้
node --version
) - Java 7 หรือสูงกว่า (ในการติดตั้ง Java ให้ใช้คำแนะนำเหล่านี้ เพื่อตรวจสอบเวอร์ชันของคุณ ให้รัน
java -version
)
สิ่งที่คุณจะทำ
ใน Codelab นี้ คุณจะปกป้องแพลตฟอร์มบล็อกง่ายๆ ที่สร้างขึ้นบน Firestore คุณจะใช้โปรแกรมจำลอง Firestore เพื่อเรียกใช้การทดสอบหน่วยกับกฎความปลอดภัย และตรวจสอบให้แน่ใจว่ากฎอนุญาตและไม่อนุญาตให้มีการเข้าถึงที่คุณคาดหวัง
คุณจะได้เรียนรู้วิธีการ:
- ให้สิทธิ์แบบละเอียด
- บังคับใช้ข้อมูลและการตรวจสอบประเภท
- ใช้การควบคุมการเข้าถึงตามแอตทริบิวต์
- ให้สิทธิ์การเข้าถึงตามวิธีการตรวจสอบสิทธิ์
- สร้างฟังก์ชั่นที่กำหนดเอง
- สร้างกฎความปลอดภัยตามเวลา
- ใช้รายการปฏิเสธและลบแบบนุ่มนวล
- ทำความเข้าใจว่าเมื่อใดควรทำให้ข้อมูลไม่ปกติเพื่อให้เป็นไปตามรูปแบบการเข้าถึงที่หลากหลาย
2. ตั้งค่า
นี่คือแอปพลิเคชั่นบล็อก นี่คือบทสรุประดับสูงของฟังก์ชันการทำงานของแอปพลิเคชัน:
โพสต์บล็อกฉบับร่าง:
- ผู้ใช้สามารถสร้างโพสต์บล็อกแบบร่างซึ่งอยู่ในคอลเล็กชัน
drafts
- ผู้เขียนสามารถอัปเดตฉบับร่างต่อไปได้จนกว่าจะพร้อมเผยแพร่
- เมื่อพร้อมที่จะเผยแพร่ ฟังก์ชัน Firebase จะทำงานเพื่อสร้างเอกสารใหม่ในคอลเล็กชัน
published
- ผู้เขียนหรือผู้ดูแลไซต์สามารถลบแบบร่างได้
โพสต์บล็อกที่เผยแพร่:
- ผู้ใช้ไม่สามารถสร้างโพสต์ที่เผยแพร่ได้ ทำได้โดยใช้ฟังก์ชันเท่านั้น
- สามารถลบได้แบบซอฟต์เท่านั้น ซึ่งจะอัปเดตแอตทริบิวต์
visible
เป็นเท็จ
ความคิดเห็น
- โพสต์ที่เผยแพร่อนุญาตให้แสดงความคิดเห็นซึ่งเป็นคอลเลกชันย่อยของโพสต์ที่เผยแพร่แต่ละรายการ
- เพื่อลดการละเมิด ผู้ใช้ต้องมีที่อยู่อีเมลที่ได้รับการยืนยันและไม่ได้เป็นผู้ปฏิเสธเพื่อที่จะแสดงความคิดเห็น
- ความคิดเห็นสามารถอัปเดตได้ภายในหนึ่งชั่วโมงหลังจากโพสต์
- ความคิดเห็นสามารถลบได้โดยผู้เขียนความคิดเห็น ผู้เขียนโพสต์ต้นฉบับ หรือโดยผู้ดูแล
นอกจากกฎการเข้าถึงแล้ว คุณจะต้องสร้างกฎความปลอดภัยที่บังคับใช้ฟิลด์ที่จำเป็นและการตรวจสอบความถูกต้องของข้อมูล
ทุกอย่างจะเกิดขึ้นภายในเครื่อง โดยใช้ Firebase Emulator Suite
รับซอร์สโค้ด
ใน Codelab นี้ คุณจะเริ่มต้นด้วยการทดสอบกฎความปลอดภัย แต่กฎความปลอดภัยจะเลียนแบบ ดังนั้นสิ่งแรกที่คุณต้องทำคือโคลนซอร์สเพื่อเรียกใช้การทดสอบ:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
จากนั้นย้ายไปยังไดเร็กทอรีสถานะเริ่มต้น ซึ่งคุณจะทำงานส่วนที่เหลือของ Codelab นี้:
$ cd codelab-rules/initial-state
ตอนนี้ ติดตั้งการพึ่งพาเพื่อให้คุณสามารถเรียกใช้การทดสอบ หากคุณใช้การเชื่อมต่ออินเทอร์เน็ตที่ช้าลง อาจใช้เวลาหนึ่งหรือสองนาที:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
รับ Firebase CLI
Emulator Suite ที่คุณจะใช้เพื่อเรียกใช้การทดสอบเป็นส่วนหนึ่งของ Firebase CLI (อินเทอร์เฟซบรรทัดคำสั่ง) ซึ่งสามารถติดตั้งบนเครื่องของคุณด้วยคำสั่งต่อไปนี้:
$ npm install -g firebase-tools
จากนั้น ยืนยันว่าคุณมี CLI เวอร์ชันล่าสุด Codelab นี้ควรทำงานร่วมกับเวอร์ชัน 8.4.0 หรือสูงกว่า แต่เวอร์ชันที่ใหม่กว่าจะมีการแก้ไขข้อบกพร่องเพิ่มเติม
$ firebase --version 9.10.2
3. เรียกใช้การทดสอบ
ในส่วนนี้ คุณจะเรียกใช้การทดสอบในเครื่อง ซึ่งหมายความว่าได้เวลาบู๊ต Emulator Suite แล้ว
เริ่มโปรแกรมจำลอง
แอปพลิเคชันที่คุณจะใช้งานมีคอลเล็กชัน Firestore หลักสามคอลเล็กชัน: drafts
มีบล็อกโพสต์ที่กำลังดำเนินการอยู่ คอลเล็กชัน published
มีบล็อกโพสต์ที่เผยแพร่แล้ว และ comments
เป็นคอลเล็กชันย่อยของโพสต์ที่เผยแพร่ Repo มาพร้อมกับการทดสอบหน่วยสำหรับกฎความปลอดภัยที่กำหนดแอตทริบิวต์ของผู้ใช้และเงื่อนไขอื่นๆ ที่จำเป็นสำหรับผู้ใช้ในการสร้าง อ่าน อัปเดต และลบเอกสารใน 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
ถัดไป เอกสารจะสามารถสร้างได้ก็ต่อเมื่อมีฟิลด์บังคับสามฟิลด์ ได้แก่ 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 รายการ ได้แก่ 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. การอัปเดตโพสต์ที่เผยแพร่: ฟังก์ชันที่กำหนดเองและตัวแปรในเครื่อง
เงื่อนไขในการปรับปรุง aa โพสต์ที่เผยแพร่คือ:
- สามารถทำได้โดยผู้เขียนหรือผู้ดำเนินรายการเท่านั้น และ
- จะต้องมีฟิลด์ที่จำเป็นทั้งหมด
เนื่องจากคุณได้เขียนเงื่อนไขสำหรับการเป็นผู้เขียนหรือผู้ดำเนินรายการแล้ว คุณสามารถคัดลอกและวางเงื่อนไขได้ แต่เมื่อเวลาผ่านไปอาจทำให้อ่านและดูแลรักษาได้ยาก คุณจะสร้างฟังก์ชันแบบกำหนดเองที่สรุปตรรกะของการเป็นผู้เขียนหรือผู้ดูแลแทน จากนั้น คุณจะเรียกใช้จากหลายเงื่อนไข
สร้างฟังก์ชันแบบกำหนดเอง
เหนือคำสั่งจับคู่สำหรับฉบับร่าง ให้สร้างฟังก์ชันใหม่ชื่อ 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 statement และฟังก์ชันของเราจะคืนค่าบูลีนเพื่อระบุว่าตัวแปรใดตัวแปรหนึ่งเป็นจริง:
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
ที่ไม่เปลี่ยนรูปแบบ ฟิลด์อีกสองฟิลด์ 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 /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";
เรียกใช้การทดสอบของคุณอีกครั้ง และยืนยันว่าผ่านการทดสอบอีกหนึ่งรายการ
การสร้างความคิดเห็น: การตรวจสอบรายการปฏิเสธ
มีสามเงื่อนไขในการสร้างความคิดเห็น:
- ผู้ใช้ต้องมีอีเมลยืนยัน
- ความคิดเห็นต้องมีอักขระน้อยกว่า 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 charachters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
รันการทดสอบอีกครั้ง และตรวจสอบให้แน่ใจว่าผ่านการทดสอบอีกหนึ่งรายการ
10. การอัปเดตความคิดเห็น: กฎตามเวลา
ตรรกะทางธุรกิจสำหรับความคิดเห็นคือผู้เขียนความคิดเห็นสามารถแก้ไขได้ภายในหนึ่งชั่วโมงหลังจากสร้าง ในการดำเนินการนี้ ให้ใช้การประทับเวลา 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');
รันการทดสอบอีกครั้ง และตรวจสอบให้แน่ใจว่าผ่านการทดสอบอีกหนึ่งรายการ
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;
รันการทดสอบอีกครั้ง และตรวจสอบให้แน่ใจว่าผ่านการทดสอบอีกหนึ่งรายการ
และไฟล์กฎทั้งหมดคือ:
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 charachters
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 : การพัฒนาครั้งแรกในพื้นที่ด้วย Emulators
- วิดีโอ : วิธีใช้การตั้งค่า CI สำหรับการทดสอบโดยใช้โปรแกรมจำลองโดยใช้ GitHub Actions