التحكم في الوصول إلى حقول محددة

تستند هذه الصفحة إلى المفاهيم الواردة في تنظيم قواعد الأمان و كتابة شروط قواعد الأمان لشرح كيفية استخدام Cloud Firestore Security Rules لإنشاء قواعد تسمح للعملاء بإجراء عمليات على بعض الحقول في المستند وليس غيرها.

قد تحتاج أحيانًا إلى التحكّم في التغييرات التي تطرأ على مستند ليس على مستوى المستند، بل على مستوى الحقل.

على سبيل المثال، قد تريد السماح لعميل بإنشاء مستند أو تغييره، ولكن لا تريد السماح له بتعديل حقول معيّنة في هذا المستند. أو يمكنك فرض أن يحتوي أي مستند ينشئه العميل دائمًا على مجموعة معيّنة من الحقول. يتناول هذا الدليل كيفية تنفيذ بعض هذه المهام باستخدام Cloud Firestore Security Rules.

السماح بالوصول للقراءة فقط في حقول معيّنة

يتم إجراء عمليات القراءة في Cloud Firestore على مستوى المستند. يمكنك إما retrieving the full document، أو retrieving nothing. لا تتوفّر طريقة لاسترداد مستند جزئي. من المستحيل استخدام قواعد الأمان وحدها لمنع المستخدمين من قراءة حقول معيّنة في مستند.

إذا كانت هناك حقول معيّنة في مستند تريد إخفاءها عن بعض المستخدمين، فإنّ أفضل طريقة هي وضعها في مستند منفصل. على سبيل المثال، يمكنك إنشاء مستند في مجموعة فرعية 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

بعد ذلك، يمكنك إضافة قواعد أمان لها مستويات وصول مختلفة للجمعتَين. في هذا المثال، نستخدم مطالبات المصادقة المخصّصة للإشارة إلى أنّه لا يمكن سوى للمستخدمين الذين لديهم مطالبة المصادقة المخصّصة 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']);
    }
  }
}

يتيح ذلك إنشاء المطاعم باستخدام حقول أخرى أيضًا، ولكنه يضمن أن تحتوي جميع المستندات التي أنشأها العميل على هذه الحقول الثلاثة على الأقل.

حظر حقول معيّنة في المستندات الجديدة

وبالمثل، يمكنك منع العملاء من إنشاء مستندات تحتوي على حقول معيّنة باستخدام hasAny() ضد قائمة بالحقول المحظورة. تُعرِض هذه الطريقة القيمة true إذا كان ال document يحتوي على أيّ من هذه الحقول، لذا قد تحتاج إلى نفي النتيجة من أجل حظر حقول معيّنة.

على سبيل المثال، في المثال التالي، لا يُسمح للعملاء بإنشاء مستند يحتوي على حقل 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 ، وهو عنصر يحتوي على جميع التغييرات بين نقشتَين مختلفتَين.

من خلال استدعاء الأسلوب 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 يؤدي إلى حدوث خطأ، وبالتالي فإنّ أي قاعدة أمان تُجري هذا الاستدعاء سترفض الطلب. يمكنك التعامل مع هذه الحالة باستخدام 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 هي أنّه يسمح لل العميل بإجراء تغيير على مستند، أو يرفض التعديل بالكامل. لا يمكنك إنشاء قواعد أمان تقبل عمليات الكتابة في بعض الحقول في الوثيقة مع رفض حقول أخرى في العملية نفسها.