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

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

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 คือการอนุมัติจะอนุญาตให้ไคลเอ็นต์ทำการเปลี่ยนแปลงเอกสาร หรือปฏิเสธการแก้ไขทั้งหมด คุณไม่สามารถสร้างกฎความปลอดภัยที่ยอมรับการเขียนไปยังบางฟิลด์ในเอกสารขณะปฏิเสธฟิลด์อื่นๆ ในการดำเนินการเดียวกัน