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

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

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

ตัวอย่างเช่น กฎต่อไปนี้ช่วยให้มั่นใจว่าช่อง 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 จะทำให้เกิดข้อผิดพลาด ดังนั้นกฎความปลอดภัยที่เรียกใช้ request.resource.data.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 คือ Cloud Firestore Security Rules จะอนุญาตให้ลูกค้าทำการเปลี่ยนแปลงในเอกสารหรือปฏิเสธการแก้ไขทั้งหมดก็ได้ คุณไม่สามารถสร้างกฎความปลอดภัยที่ยอมรับการเขียนลงในบางช่องในเอกสารขณะที่ปฏิเสธการเขียนในช่องอื่นๆ ในการดำเนินการเดียวกัน