การเขียนเงื่อนไขสำหรับกฎความปลอดภัยของ Cloud Firestore

คำแนะนำนี้ต่อยอดมาจากคำแนะนำการจัดโครงสร้างกฎความปลอดภัยเพื่อแสดงวิธีเพิ่มเงื่อนไขลงใน Cloud Firestore Security Rules หากยังไม่คุ้นเคยกับข้อมูลเบื้องต้นเกี่ยวกับ Cloud Firestore Security Rules โปรดดูคู่มือการเริ่มต้นใช้งาน

องค์ประกอบพื้นฐานหลักของ Cloud Firestore Security Rules คือเงื่อนไข เงื่อนไขคือนิพจน์บูลีนที่กำหนดว่าควรอนุญาตหรือปฏิเสธการดำเนินการหนึ่งๆ ใช้กฎความปลอดภัยเพื่อเขียนเงื่อนไขที่จะตรวจสอบการตรวจสอบสิทธิ์ของผู้ใช้ ตรวจสอบข้อมูลที่เข้ามา หรือแม้แต่เข้าถึงส่วนอื่นๆ ของฐานข้อมูล

การตรวจสอบสิทธิ์

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

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to access documents in the "cities" collection
    // only if they are authenticated.
    match /cities/{city} {
      allow read, write: if request.auth != null;
    }
  }
}

รูปแบบที่พบบ่อยอีกอย่างหนึ่งคือการตรวจสอบว่าผู้ใช้อ่านและเขียนได้เฉพาะข้อมูลของตนเองเท่านั้น

service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches name of the user
    // document. The wildcard expression {userId} makes the userId variable
    // available in rules.
    match /users/{userId} {
      allow read, update, delete: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null;
    }
  }
}

หากแอปใช้การตรวจสอบสิทธิ์ Firebase หรือ Google Cloud Identity Platform ตัวแปร request.auth จะมีข้อมูลการตรวจสอบสิทธิ์สําหรับไคลเอ็นต์ที่ขอข้อมูล ดูข้อมูลเพิ่มเติมเกี่ยวกับ request.auth ได้ที่เอกสารอ้างอิง

การตรวจสอบข้อมูล

แอปจำนวนมากจัดเก็บข้อมูลการควบคุมการเข้าถึงเป็นช่องในเอกสารในฐานข้อมูล Cloud Firestore Security Rules สามารถอนุญาตหรือปฏิเสธการเข้าถึงแบบไดนามิกตามข้อมูลเอกสาร ดังนี้

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to read data if the document has the 'visibility'
    // field set to 'public'
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
    }
  }
}

ตัวแปร resource หมายถึงเอกสารที่ขอ และ resource.data คือแผนที่ของช่องและค่าทั้งหมดที่จัดเก็บไว้ในเอกสาร ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวแปร resource ได้ที่เอกสารอ้างอิง

เมื่อเขียนข้อมูล คุณอาจต้องเปรียบเทียบข้อมูลที่เข้ามากับข้อมูลที่มีอยู่ ในกรณีนี้ หากชุดกฎอนุญาตการเขียนที่รอดำเนินการ ตัวแปร request.resource จะมีสถานะในอนาคตของเอกสาร สําหรับการดำเนินการ update ที่แก้ไขเฉพาะชุดย่อยของช่องเอกสาร ตัวแปร request.resource จะมีสถานะเอกสารที่รอดําเนินการหลังจากการดำเนินการ คุณสามารถตรวจสอบค่าในช่อง request.resource เพื่อป้องกันไม่ให้ระบบอัปเดตข้อมูลที่ไม่ต้องการหรือไม่สอดคล้องกัน โดยทำดังนี้

service cloud.firestore {
  match /databases/{database}/documents {
    // Make sure all cities have a positive population and
    // the name is not changed
    match /cities/{city} {
      allow update: if request.resource.data.population > 0
                    && request.resource.data.name == resource.data.name;
    }
  }
}

เข้าถึงเอกสารอื่นๆ

เมื่อใช้ฟังก์ชัน get() และ exists() กฎความปลอดภัยจะประเมินคำขอขาเข้าเทียบกับเอกสารอื่นๆ ในฐานข้อมูลได้ ทั้งฟังก์ชัน get() และ exists() ต้องการเส้นทางเอกสารที่ระบุอย่างครบถ้วน เมื่อใช้ตัวแปรเพื่อสร้างเส้นทางสําหรับ get() และ exists() คุณต้องหลีกตัวแปรอย่างชัดเจนโดยใช้ไวยากรณ์ $(variable)

ในตัวอย่างนี้ ตัวแปร database จะถูกบันทึกโดยคำสั่งการจับคู่ match /databases/{database}/documents และใช้เพื่อสร้างเส้นทาง

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      // Make sure a 'users' document exists for the requesting user before
      // allowing any writes to the 'cities' collection
      allow create: if request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid));

      // Allow the user to delete cities if their user document has the
      // 'admin' field set to 'true'
      allow delete: if request.auth != null && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;
    }
  }
}

สำหรับการเขียน คุณสามารถใช้ฟังก์ชัน getAfter() เพื่อเข้าถึงสถานะของเอกสารหลังจากที่ธุรกรรมหรือการเขียนกลุ่มหนึ่งเสร็จสมบูรณ์แล้ว แต่ก่อนที่ธุรกรรมหรือการเขียนกลุ่มหนึ่งจะได้รับการคอมมิต เช่นเดียวกับ get() ฟังก์ชัน getAfter() จะรับเส้นทางของเอกสารที่ระบุอย่างสมบูรณ์ คุณสามารถใช้ getAfter() เพื่อกำหนดชุดการเขียนที่ต้องดำเนินการร่วมกันเป็นธุรกรรมหรือกลุ่ม

เข้าถึงขีดจำกัดการโทร

การเรียกใช้การเข้าถึงเอกสารต่อการประเมินชุดกฎมีขีดจํากัดดังนี้

  • 10 รายการสำหรับคำขอเอกสารรายการเดียวและคำขอการค้นหา
  • 20 สำหรับรายการที่อ่านหลายรายการ ธุรกรรม และการเขียนแบบเป็นกลุ่ม การดำเนินการแต่ละรายการยังมีขีดจำกัดเดิมที่ 10 รายการด้วย

    ตัวอย่างเช่น สมมติว่าคุณสร้างคำขอเขียนแบบเป็นกลุ่มที่มีการดำเนินการเขียน 3 รายการ และกฎการรักษาความปลอดภัยใช้การเรียกใช้การเข้าถึงเอกสาร 2 ครั้งเพื่อตรวจสอบการเขียนแต่ละรายการ ในกรณีนี้ การเขียนแต่ละรายการใช้การเรียกใช้การเข้าถึง 2 ครั้งจาก 10 ครั้ง และคำขอเขียนแบบเป็นกลุ่มใช้การเรียกใช้การเข้าถึง 6 ครั้งจาก 20 ครั้ง

หากเกินขีดจำกัดดังกล่าว ระบบจะแสดงข้อผิดพลาด "ถูกปฏิเสธสิทธิ์" ระบบอาจแคชการเรียกใช้การเข้าถึงเอกสารบางรายการไว้ และการเรียกใช้ที่แคชไว้จะไม่นับรวมในขีดจำกัด

ดูคำอธิบายโดยละเอียดว่าขีดจํากัดเหล่านี้ส่งผลต่อธุรกรรมและการเขียนแบบเป็นกลุ่มอย่างไรได้ที่คู่มือการรักษาความปลอดภัยให้กับการดำเนินการแบบอะตอม

การเข้าถึงการโทรและการกำหนดราคา

การใช้ฟังก์ชันเหล่านี้จะดำเนินการอ่านในฐานข้อมูล ซึ่งหมายความว่าระบบจะเรียกเก็บเงินจากคุณสำหรับการอ่านเอกสารแม้ว่ากฎจะปฏิเสธคำขอก็ตาม ดูข้อมูลการเรียกเก็บเงินที่เฉพาะเจาะจงเพิ่มเติมได้ที่ราคา Cloud Firestore

ฟังก์ชันที่กำหนดเอง

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

  • ฟังก์ชันจะมีคำสั่ง return ได้เพียงรายการเดียว โดยต้องไม่มีตรรกะเพิ่มเติม เช่น ไม่สามารถเรียกใช้ลูปหรือเรียกใช้บริการภายนอก
  • ฟังก์ชันจะเข้าถึงฟังก์ชันและตัวแปรจากขอบเขตที่กําหนดไว้โดยอัตโนมัติ ตัวอย่างเช่น ฟังก์ชันที่กําหนดภายในขอบเขต service cloud.firestore จะมีสิทธิ์เข้าถึงตัวแปร resource และฟังก์ชันในตัว เช่น get() และ exists()
  • ฟังก์ชันอาจเรียกใช้ฟังก์ชันอื่นๆ แต่เรียกซ้ำไม่ได้ ระดับความลึกของกองซ้อนการเรียกทั้งหมดถูกจํากัดไว้ที่ 10
  • ในกฎเวอร์ชัน v2 ฟังก์ชันจะกําหนดตัวแปรได้โดยใช้คีย์เวิร์ด let ฟังก์ชันมีการเชื่อมโยง let ได้สูงสุด 10 รายการ แต่ต้องลงท้ายด้วยคำสั่ง return

ฟังก์ชันจะกำหนดด้วยคีย์เวิร์ด function และใช้อาร์กิวเมนต์ได้ตั้งแต่ 0 รายการขึ้นไป เช่น คุณอาจต้องการรวมเงื่อนไข 2 ประเภทที่ใช้ในตัวอย่างข้างต้นเข้าด้วยกันเป็นฟังก์ชันเดียว

service cloud.firestore {
  match /databases/{database}/documents {
    // True if the user is signed in or the requested data is 'public'
    function signedInOrPublic() {
      return request.auth.uid != null || resource.data.visibility == 'public';
    }

    match /cities/{city} {
      allow read, write: if signedInOrPublic();
    }

    match /users/{user} {
      allow read, write: if signedInOrPublic();
    }
  }
}

การใช้ฟังก์ชันในกฎการรักษาความปลอดภัยจะช่วยให้กฎเหล่านั้นบำรุงรักษาได้ง่ายขึ้นเมื่อกฎมีความซับซ้อนมากขึ้น

กฎไม่ใช่ตัวกรอง

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

ตัวอย่างเช่น มาดูกฎความปลอดภัยต่อไปนี้

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to read data if the document has the 'visibility'
    // field set to 'public'
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
    }
  }
}

ถูกปฏิเสธ: กฎนี้จะปฏิเสธการค้นหาต่อไปนี้เนื่องจากชุดผลลัพธ์อาจรวมเอกสารที่ visibility ไม่ใช่ public

เว็บ
db.collection("cities").get()
    .then(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
            console.log(doc.id, " => ", doc.data());
    });
});

อนุญาต: กฎนี้อนุญาตการค้นหาต่อไปนี้เนื่องจากประโยค where("visibility", "==", "public") รับประกันว่าชุดผลลัพธ์จะเป็นไปตามเงื่อนไขของกฎ

เว็บ
db.collection("cities").where("visibility", "==", "public").get()
    .then(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
            console.log(doc.id, " => ", doc.data());
        });
    });

กฎความปลอดภัยของ Cloud Firestore จะประเมินการค้นหาแต่ละรายการเทียบกับผลลัพธ์ที่เป็นไปได้ และปฏิเสธคำขอหากอาจแสดงผลลัพธ์เป็นเอกสารที่ไคลเอ็นต์ไม่มีสิทธิ์อ่าน การค้นหาต้องเป็นไปตามข้อจำกัดที่กำหนดโดยกฎการรักษาความปลอดภัย ดูข้อมูลเพิ่มเติมเกี่ยวกับกฎความปลอดภัยและการค้นหาได้ที่การค้นหาข้อมูลอย่างปลอดภัย

ขั้นตอนถัดไป