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

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

ข้อกำหนดที่สองสำหรับการอัปเดตคือแอตทริบิวต์สองรายการ 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

เนื่องจากเงื่อนไขข้อใดข้อหนึ่งเหล่านี้เพียงพอสำหรับการลบ ให้ต่อเงื่อนไขเหล่านั้นเข้ากับตัวดำเนินการเชิงตรรกะหรือ || : :

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 และฟังก์ชันของเราจะส่งคืนบูลีนเพื่อระบุว่าตัวแปรตัวใดตัวหนึ่งเป็นจริงหรือไม่:

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:

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 characters
        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

เนื่องจากเงื่อนไขใดๆ เหล่านี้เพียงพอแล้ว ให้ใช้ตัวดำเนินการเชิงตรรกะหรือระหว่างเงื่อนไขเหล่านี้:

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