ควบคุมการเข้าถึงช่องที่ต้องการ

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

บางครั้งคุณอาจต้องการควบคุมการเปลี่ยนแปลงในเอกสารที่ระดับช่อง ไม่ใช่ระดับเอกสาร

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

อนุญาตสิทธิ์เข้าถึงแบบอ่านสำหรับบางช่องเท่านั้น

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

หากมีบางช่องในเอกสารที่คุณต้องการซ่อนจากผู้ใช้บางราย วิธีที่ดีที่สุดคือใส่ช่องเหล่านั้นไว้ในเอกสารแยกต่างหาก เช่น คุณอาจพิจารณาสร้างเอกสารในคอลเล็กชันย่อย private ดังนี้

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

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

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

จำกัดช่องเมื่อสร้างเอกสาร

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

คุณสามารถสร้างกฎเหล่านี้ได้โดยการตรวจสอบเมธอด keys ของ request.resource.data ออบเจ็กต์ ซึ่งเป็นรายการช่องทั้งหมดที่ไคลเอ็นต์พยายามเขียนในเอกสารใหม่นี้ การรวมชุดช่องนี้ เข้ากับฟังก์ชันต่างๆ เช่น hasOnly() หรือ hasAny() จะช่วยให้คุณเพิ่มตรรกะที่จำกัดประเภทเอกสารที่ผู้ใช้เพิ่มลงใน Cloud Firestoreได้

กำหนดให้เอกสารใหม่ต้องมีบางช่อง

สมมติว่าคุณต้องการตรวจสอบว่าเอกสารทั้งหมดที่สร้างในคอลเล็กชัน restaurant มีช่อง name, location และ city อย่างน้อย คุณสามารถ ทำได้โดยเรียก hasAll() ในรายการคีย์ในเอกสารใหม่

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

วิธีนี้จะอนุญาตให้สร้างร้านอาหารที่มีช่องอื่นๆ ได้ด้วย แต่จะตรวจสอบว่าเอกสารทั้งหมดที่ไคลเอ็นต์สร้างขึ้นมีช่อง 3 ช่องนี้อย่างน้อย

ห้ามไม่ให้เอกสารใหม่มีบางช่อง

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

ตัวอย่างเช่น ในตัวอย่างต่อไปนี้ ไคลเอ็นต์ไม่ได้รับอนุญาตให้สร้างเอกสารที่มีช่อง average_score หรือ rating_count เนื่องจากระบบจะเพิ่มช่องเหล่านี้โดยการเรียกเซิร์ฟเวอร์ในภายหลัง

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

สร้างรายการที่อนุญาตของช่องสำหรับเอกสารใหม่

คุณอาจต้องการสร้างรายการเฉพาะช่องที่ได้รับอนุญาตอย่างชัดเจนในเอกสารใหม่แทนที่จะห้ามบางช่องในเอกสารใหม่ จากนั้น คุณสามารถใช้ hasOnly() ฟังก์ชัน เพื่อตรวจสอบว่าเอกสารใหม่ที่สร้างขึ้นมีเฉพาะช่องเหล่านี้ (หรือชุดย่อยของช่องเหล่านี้) และไม่มีช่องอื่นๆ

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

รวมช่องที่ต้องกรอกและช่องที่ไม่บังคับ

คุณสามารถรวมการดำเนินการ hasAll และ hasOnly ไว้ด้วยกันในกฎความปลอดภัยเพื่อกำหนดให้มีบางช่องและอนุญาตให้มีช่องอื่นๆ ได้ ตัวอย่างเช่น ตัวอย่างนี้กำหนดให้เอกสารใหม่ทั้งหมดต้องมีช่อง name, location และ city และอนุญาตให้มีช่อง address, hours และ cuisine ได้

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

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

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

จำกัดช่องเมื่ออัปเดต

แนวทางปฏิบัติที่พบบ่อยด้านความปลอดภัยคือการอนุญาตให้ไคลเอ็นต์แก้ไขบางช่องเท่านั้น คุณไม่สามารถทำเช่นนี้ได้โดยดูเฉพาะรายการ request.resource.data.keys() ที่อธิบายไว้ในส่วนก่อนหน้า เนื่องจากรายการนี้แสดงเอกสารฉบับเต็มตามที่จะปรากฏหลังจากการอัปเดต และจะรวมช่องที่ไคลเอ็นต์ไม่ได้เปลี่ยนแปลงด้วย

อย่างไรก็ตาม หากคุณใช้diff() ฟังก์ชัน คุณจะเปรียบเทียบ request.resource.data กับ resource.data ออบเจ็กต์ได้ ซึ่งแสดงเอกสารในฐานข้อมูลก่อน การอัปเดต ซึ่งจะสร้างออบเจ็กต์ mapDiff ซึ่งเป็นออบเจ็กต์ที่มีการเปลี่ยนแปลงทั้งหมดระหว่าง 2 แผนที่ที่แตกต่างกัน

การเรียกเมธอด affectedKeys() ใน mapDiff นี้จะช่วยให้คุณได้ชุดช่องที่เปลี่ยนแปลง ในการแก้ไข จากนั้นคุณสามารถใช้ฟังก์ชันต่างๆ เช่น hasOnly() หรือ hasAny() เพื่อตรวจสอบว่าชุดนี้มี (หรือไม่) บางรายการ

ป้องกันไม่ให้มีการเปลี่ยนแปลงบางช่อง

การใช้เมธอด hasAny() ในชุดที่สร้างโดย affectedKeys() แล้วปฏิเสธผลลัพธ์จะช่วยให้คุณปฏิเสธคำขอของไคลเอ็นต์ที่พยายาม เปลี่ยนแปลงช่องที่คุณไม่ต้องการให้เปลี่ยนแปลงได้

ตัวอย่างเช่น คุณอาจต้องการอนุญาตให้ไคลเอ็นต์อัปเดตข้อมูลเกี่ยวกับร้านอาหารได้ แต่ไม่อนุญาตให้เปลี่ยนแปลงคะแนนเฉลี่ยหรือจำนวนรีวิว

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

อนุญาตให้เปลี่ยนแปลงบางช่องเท่านั้น

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

ตัวอย่างเช่น แทนที่จะไม่อนุญาตช่อง average_score และ rating_count คุณสามารถสร้างกฎความปลอดภัยที่อนุญาตให้ไคลเอ็นต์เปลี่ยนแปลงเฉพาะช่อง name, location, city, address, hours และ cuisine

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

ซึ่งหมายความว่าหากเอกสารร้านอาหารมีช่อง telephone ในการทำซ้ำแอปในอนาคต การพยายามแก้ไขช่องนั้นจะล้มเหลวจนกว่าคุณจะกลับไปเพิ่มช่องนั้นลงในรายการ hasOnly() ในกฎความปลอดภัย

บังคับใช้ประเภทช่อง

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

ตัวอย่างเช่น กฎความปลอดภัยต่อไปนี้บังคับใช้ว่าช่อง score ของรีวิวต้องเป็นจำนวนเต็ม ช่อง headline, content และ author_name ต้องเป็นสตริง และ review_date ต้องเป็นค่า Timestamp

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

ประเภทข้อมูลที่ถูกต้องสำหรับโอเปอเรเตอร์ is ได้แก่ bool, bytes, float, int, list, latlng, number, path, map, string และ timestamp นอกจากนี้ โอเปอเรเตอร์ is ยังรองรับประเภทข้อมูล constraint, duration, set และ map_diff ด้วย แต่เนื่องจากประเภทข้อมูลเหล่านี้สร้างขึ้นโดยภาษาของกฎความปลอดภัยเอง ไม่ได้สร้างโดยไคลเอ็นต์ คุณจึงไม่ค่อยได้ใช้ประเภทข้อมูลเหล่านี้ในการใช้งานจริงส่วนใหญ่

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

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

ตัวอย่างเช่น กฎต่อไปนี้จะตรวจสอบว่าช่อง tags ในเอกสาร มีรายการและรายการแรกเป็นสตริง นอกจากนี้ยังตรวจสอบว่าช่อง product มีแผนที่ซึ่งมีชื่อสินค้าที่เป็นสตริงและจำนวนที่เป็นจำนวนเต็ม

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

คุณต้องบังคับใช้ประเภทช่องเมื่อสร้างและอัปเดตเอกสาร ดังนั้น คุณอาจพิจารณาสร้างฟังก์ชันตัวช่วยที่คุณเรียกได้ทั้งในส่วนการสร้างและการอัปเดตของกฎความปลอดภัย

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

บังคับใช้ประเภทสำหรับช่องที่ไม่บังคับ

โปรดทราบว่าการเรียก request.resource.data.foo ใน เอกสารที่ไม่มี foo จะทำให้เกิดข้อผิดพลาด และกฎความปลอดภัยใดก็ตามที่เรียกดังกล่าวจะปฏิเสธคำขอ คุณสามารถจัดการสถานการณ์นี้ได้โดยใช้เมธอด getใน request.resource.data เมธอด get ช่วยให้คุณระบุอาร์กิวเมนต์เริ่มต้นสำหรับช่องที่คุณดึงข้อมูลจากแผนที่ได้หากไม่มีช่องนั้น

ตัวอย่างเช่น หากเอกสารรีวิวยังมีช่อง photo_url ที่ไม่บังคับและช่อง tags ที่ไม่บังคับที่คุณต้องการตรวจสอบว่าเป็นสตริงและรายการตามลำดับ คุณสามารถทำได้โดยการเขียนฟังก์ชัน reviewFieldsAreValidTypes ใหม่ให้มีลักษณะดังต่อไปนี้

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

ซึ่งจะปฏิเสธเอกสารที่มี tags แต่ไม่ใช่รายการ ขณะเดียวกันก็ยังอนุญาตเอกสารที่ไม่มีช่อง tags (หรือ photo_url)

ระบบไม่อนุญาตการเขียนบางส่วน

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