ปกป้องข้อมูลใน Firestore ด้วยกฎความปลอดภัยของ Firebase

1. ก่อนเริ่มต้น

Cloud Firestore, Cloud Storage for Firebase และ Realtime Database จะใช้ไฟล์การกำหนดค่าที่คุณเขียนเพื่อให้สิทธิ์อ่านและเขียน การกำหนดค่าดังกล่าว ซึ่งเรียกว่า "กฎความปลอดภัย" สามารถทำหน้าที่เป็นสคีมาประเภทหนึ่งสำหรับแอปของคุณได้เช่นกัน ซึ่งเป็นส่วนที่สำคัญที่สุดส่วนหนึ่งในการพัฒนาแอปพลิเคชัน และ Codelab นี้จะแนะนำวิธีการให้กับคุณ

ข้อกำหนดเบื้องต้น

  • โปรแกรมแก้ไขแบบง่าย เช่น โค้ด Visual Studio, Atom หรือ Sublime Text
  • Node.js 8.6.0 ขึ้นไป (หากต้องการติดตั้ง Node.js ให้ใช้ nvm หากต้องการตรวจสอบเวอร์ชัน ให้ใช้ node --version)
  • Java 7 ขึ้นไป (หากต้องการติดตั้ง Java ใช้คำแนะนำ หากต้องการตรวจสอบเวอร์ชัน ให้เรียกใช้ java -version)

สิ่งที่คุณจะต้องทำ

ใน Codelab นี้ คุณจะได้รักษาความปลอดภัยแพลตฟอร์มบล็อกง่ายๆ ที่สร้างขึ้นจาก Firestore คุณจะใช้โปรแกรมจำลอง Firestore เพื่อทำการทดสอบ 1 หน่วยกับกฎการรักษาความปลอดภัย และตรวจสอบว่ากฎอนุญาตและไม่อนุญาตการเข้าถึงที่คุณต้องการ

โดยคุณจะได้เรียนรู้วิธีต่อไปนี้

  • ให้สิทธิ์แบบละเอียด
  • บังคับใช้การตรวจสอบข้อมูลและประเภท
  • ใช้การควบคุมการเข้าถึงตามแอตทริบิวต์
  • ให้สิทธิ์เข้าถึงตามวิธีการตรวจสอบสิทธิ์
  • สร้างฟังก์ชันที่กำหนดเอง
  • สร้างกฎความปลอดภัยตามเวลา
  • ใช้รายการปฏิเสธและทำเครื่องหมายว่าลบ
  • ทําความเข้าใจว่าเมื่อใดควรเปลี่ยนค่าปกติของข้อมูลเพื่อให้ตรงกับรูปแบบการเข้าถึงหลายรูปแบบ

2. ตั้งค่า

แอปพลิเคชันนี้เป็นการเขียนบล็อก ต่อไปนี้เป็นข้อมูลสรุประดับสูงเกี่ยวกับฟังก์ชันการทำงานของแอปพลิเคชัน:

บล็อกโพสต์ฉบับร่าง

  • ผู้ใช้จะสร้างบล็อกโพสต์ฉบับร่างซึ่งอยู่ในคอลเล็กชัน drafts ได้
  • ผู้เขียนจะอัปเดตฉบับร่างต่อไปได้จนกว่าจะพร้อมเผยแพร่
  • เมื่อพร้อมเผยแพร่ ระบบจะทริกเกอร์ฟังก์ชัน Firebase ซึ่งจะสร้างเอกสารใหม่ในคอลเล็กชัน published
  • ผู้เขียนหรือผู้ดูแลเว็บไซต์ลบฉบับร่างได้

บล็อกโพสต์ที่เผยแพร่

  • ผู้ใช้จะสร้างโพสต์ที่เผยแพร่แล้วผ่านฟังก์ชันไม่ได้ ต้องทำผ่านฟังก์ชันเท่านั้น
  • แต่จะลบแบบชั่วคราวได้เท่านั้น ซึ่งจะอัปเดตแอตทริบิวต์ visible เป็น "เท็จ"

ความคิดเห็น

  • โพสต์ที่เผยแพร่แล้วอนุญาตให้มีการแสดงความคิดเห็น ซึ่งเป็นคอลเล็กชันย่อยในแต่ละโพสต์ที่เผยแพร่
  • เพื่อลดการละเมิด ผู้ใช้ต้องมีอีเมลที่ได้รับการยืนยันและไม่อยู่ในรายชื่อผู้ที่ปฏิเสธจึงจะแสดงความคิดเห็นได้
  • อัปเดตความคิดเห็นได้ภายใน 1 ชั่วโมงหลังจากโพสต์
  • ผู้เขียนความคิดเห็น ผู้เขียนโพสต์ต้นฉบับ หรือผู้ดูแลสามารถลบความคิดเห็นได้

นอกเหนือจากกฎการเข้าถึงแล้ว คุณจะต้องสร้างกฎความปลอดภัยที่บังคับใช้ช่องที่ต้องกรอกและการตรวจสอบข้อมูล

ทุกอย่างจะเกิดขึ้นภายในเครื่องโดยใช้ Firebase Emulator Suite

รับซอร์สโค้ด

ใน Codelab นี้ คุณจะเริ่มต้นด้วยการทดสอบกฎความปลอดภัย แต่จะคล้ายกับกฎการรักษาความปลอดภัย ดังนั้นสิ่งแรกที่คุณต้องทำก็คือการโคลนแหล่งที่มาเพื่อทำการทดสอบ

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

จากนั้นย้ายไปยังไดเรกทอรีสถานะเริ่มต้น ซึ่งคุณจะทำงานให้กับส่วนที่เหลือของ Codelab นี้:

$ cd codelab-rules/initial-state

ต่อไป ให้ติดตั้งทรัพยากร Dependency เพื่อทำการทดสอบ หากการเชื่อมต่ออินเทอร์เน็ตช้า อาจใช้เวลาสักครู่

# 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 คือคอลเล็กชันย่อยของโพสต์ที่เผยแพร่แล้ว ที่เก็บมาพร้อมกับการทดสอบ 1 หน่วยสำหรับกฎความปลอดภัยที่กำหนดแอตทริบิวต์ของผู้ใช้และเงื่อนไขอื่นๆ ที่จำเป็นสำหรับการสร้าง อ่าน อัปเดต และลบเอกสารในคอลเล็กชัน 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 /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";

ทำการทดสอบอีกครั้งและยืนยันว่าการทดสอบผ่านอีก 1 รายการ

การสร้างความคิดเห็น: การตรวจสอบรายการปฏิเสธ

การสร้างความคิดเห็นมีเงื่อนไข 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: แนะนำการพัฒนาครั้งแรกในท้องถิ่นด้วยโปรแกรมจำลอง
  • วิดีโอ: วิธีใช้การตั้งค่า CI สำหรับการทดสอบในโปรแกรมจำลองโดยใช้การดำเนินการของ GitHub