قم بحماية بيانات Firestore الخاصة بك باستخدام قواعد أمان Firebase

1. قبل أن تبدأ

تعتمد Cloud Firestore و Cloud Storage for Firebase و Realtime Database على ملفات التكوين التي تكتبها لمنح حق الوصول للقراءة والكتابة. يمكن أن يعمل هذا التكوين ، المسمى قواعد الأمان ، كنوع من مخطط لتطبيقك. إنه أحد أهم أجزاء تطوير تطبيقك. وسوف يرشدك مختبر الرموز هذا من خلاله.

المتطلبات الأساسية

  • محرر بسيط مثل Visual Studio Code أو Atom أو Sublime Text
  • Node.js 8.6.0 أو أعلى (لتثبيت Node.js ، استخدم nvm ؛ للتحقق من إصدارك ، قم بتشغيل node --version )
  • Java 7 أو أعلى (لتثبيت Java ، استخدم هذه التعليمات ؛ للتحقق من الإصدار الخاص بك ، قم بتشغيل java -version )

ماذا ستفعل

في مختبر الرموز هذا ، ستؤمن منصة مدونة بسيطة مبنية على Firestore. ستستخدم محاكي Firestore لإجراء اختبارات الوحدة مقابل قواعد الأمان ، والتأكد من أن القواعد تسمح بالوصول الذي تتوقعه وترفضه.

ستتعلم كيفية:

  • منح أذونات حبيبية
  • فرض البيانات واكتب عمليات التحقق
  • تنفيذ التحكم في الوصول المستند إلى السمات
  • منح حق الوصول بناءً على طريقة المصادقة
  • إنشاء وظائف مخصصة
  • قم بإنشاء قواعد أمان تستند إلى الوقت
  • تنفيذ قائمة الرفض والحذف الناعم
  • تعرف على وقت إلغاء تنسيق البيانات لتلبية أنماط الوصول المتعددة

2. اقامة

هذا هو تطبيق التدوين. فيما يلي ملخص عالي المستوى لوظائف التطبيق:

مسودات منشورات المدونة:

  • يمكن للمستخدمين إنشاء مسودات منشورات المدونة ، والتي توجد في مجموعة drafts .
  • يمكن للمؤلف الاستمرار في تحديث مسودة حتى تصبح جاهزة للنشر.
  • عندما يكون جاهزًا للنشر ، يتم تشغيل وظيفة Firebase التي تنشئ مستندًا جديدًا في المجموعة published .
  • يمكن حذف المسودات من قبل المؤلف أو المشرفين على الموقع

منشورات المدونة المنشورة:

  • لا يمكن إنشاء المشاركات المنشورة من قبل المستخدمين ، فقط من خلال وظيفة.
  • لا يمكن حذفها إلا بشكل بسيط ، مما يؤدي إلى تحديث السمة visible إلى false.

تعليقات

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

بالإضافة إلى قواعد الوصول ، ستنشئ قواعد أمان تفرض الحقول المطلوبة وعمليات التحقق من صحة البيانات.

كل شيء سيحدث محليًا باستخدام Firebase Emulator Suite.

احصل على الكود المصدري

في مختبر الرموز هذا ، ستبدأ باختبارات لقواعد الأمان ، ولكن قواعد الأمان المحاكاة نفسها ، لذا فإن أول شيء عليك القيام به هو استنساخ المصدر لإجراء الاختبارات:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

ثم انتقل إلى دليل الحالة الأولية ، حيث ستعمل لبقية مختبر الرموز هذا:

$ cd codelab-rules/initial-state

الآن ، قم بتثبيت التبعيات حتى تتمكن من إجراء الاختبارات. إذا كنت تستخدم اتصالاً بطيئًا بالإنترنت ، فقد يستغرق ذلك دقيقة أو دقيقتين:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

احصل على Firebase CLI

تعد Emulator Suite التي ستستخدمها لإجراء الاختبارات جزءًا من Firebase CLI (واجهة سطر الأوامر) والتي يمكن تثبيتها على جهازك باستخدام الأمر التالي:

$ npm install -g firebase-tools

بعد ذلك ، تأكد من أن لديك أحدث إصدار من CLI. يجب أن يعمل مختبر الرموز هذا مع الإصدار 8.4.0 أو أعلى ولكن الإصدارات الأحدث تتضمن المزيد من إصلاحات الأخطاء.

$ firebase --version
9.10.2

3. قم بإجراء الاختبارات

في هذا القسم ، ستقوم بإجراء الاختبارات محليًا. هذا يعني أن الوقت قد حان لبدء تشغيل Emulator Suite.

ابدأ المحاكيات

يحتوي التطبيق الذي ستعمل معه على ثلاث مجموعات رئيسية من Firestore: تحتوي drafts على منشورات مدونة قيد التقدم ، وتحتوي المجموعة published على منشورات المدونة التي تم نشرها ، comments عبارة عن مجموعة فرعية من المنشورات المنشورة. يأتي الريبو مع اختبارات الوحدة لقواعد الأمان التي تحدد سمات المستخدم والشروط الأخرى المطلوبة للمستخدم لإنشاء المستندات وقراءتها وتحديثها وحذفها في مجموعات drafts published comments . ستكتب قواعد الأمان لإجراء هذه الاختبارات بنجاح.

للبدء ، تم تأمين قاعدة البيانات الخاصة بك: يتم رفض القراءة والكتابة في قاعدة البيانات عالميًا ، وتفشل جميع الاختبارات. أثناء كتابة قواعد الأمان ، ستجتاز الاختبارات. لمشاهدة الاختبارات ، افتح functions/test.js في المحرر الخاص بك.

في سطر الأوامر ، ابدأ تشغيل المحاكيات باستخدام emulators:exec وقم بإجراء الاختبارات:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

قم بالتمرير إلى أعلى الإخراج:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

يوجد الآن 9 إخفاقات. أثناء إنشاء ملف القواعد ، يمكنك قياس التقدم من خلال مشاهدة المزيد من الاختبارات تجتاز.

4. إنشاء مسودات نشر مدونة.

نظرًا لأن الوصول إلى منشورات المدونة يختلف كثيرًا عن الوصول إلى منشورات المدونة المنشورة ، فإن تطبيق التدوين هذا يخزن مسودات منشورات المدونة في مجموعة منفصلة ، /drafts . لا يمكن الوصول إلى المسودات إلا من قبل المؤلف أو الوسيط ، ولديها عمليات التحقق من صحة الحقول المطلوبة وغير القابلة للتغيير.

عند فتح ملف firestore.rules ، ستجد ملف القواعد الافتراضية:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

بيان المطابقة ، match /{document=**} ، يستخدم ** بناء الجملة لتطبيق بشكل متكرر على جميع المستندات في المجموعات الفرعية. ولأنها في المستوى الأعلى ، تنطبق نفس القاعدة الشاملة الآن على جميع الطلبات ، بغض النظر عمن يقدم الطلب أو البيانات التي يحاولون قراءتها أو كتابتها.

ابدأ بإزالة العبارة الأكثر تطابقًا واستبدالها بـ match /drafts/{draftID} . (يمكن أن تكون التعليقات الخاصة بهيكل المستندات مفيدة في القواعد ، وسيتم تضمينها في مختبر الرموز هذا ؛ وهي دائمًا اختيارية.)

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

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

الشرط الأول للإنشاء سيكون:

request.resource.data.authorUID == request.auth.uid

بعد ذلك ، لا يمكن إنشاء المستندات إلا إذا كانت تتضمن الحقول الثلاثة المطلوبة ، authorUID ، createdAt ، title . (لا يوفر المستخدم الحقل createdAt ؛ هذا يفرض على التطبيق إضافته قبل محاولة إنشاء مستند.) نظرًا لأنك تحتاج فقط إلى التحقق من إنشاء السمات ، يمكنك التحقق من هذا request.resource على كل شيء تلك المفاتيح:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

الشرط الأخير لإنشاء منشور مدونة هو ألا يزيد طول العنوان عن 50 حرفًا:

request.resource.data.title.size() < 50

نظرًا لأن كل هذه الشروط يجب أن تكون صحيحة ، قم بتوصيلها مع عامل التشغيل المنطقي و && . تصبح القاعدة الأولى:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

في المحطة ، أعد إجراء الاختبارات وتأكد من اجتياز الاختبار الأول.

5. تحديث مسودات ما بعد المدونة.

بعد ذلك ، بينما يقوم المؤلفون بتنقيح مسودات منشورات المدونة الخاصة بهم ، فسوف يقومون بتحرير مسودات المستندات. قم بإنشاء قاعدة للشروط التي يمكن فيها تحديث المنشور. أولاً ، يمكن للمؤلف فقط تحديث مسوداته. لاحظ أنك هنا تتحقق من UID المكتوب بالفعل ، resource.data.authorUID :

resource.data.authorUID == request.auth.uid

الشرط الثاني للتحديث هو أنه لا ينبغي تغيير سمتين ، authorUID و createdAt :

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

وأخيرًا ، يجب أن يتكون العنوان من 50 حرفًا أو أقل:

request.resource.data.title.size() < 50;

نظرًا لأن جميع هذه الشروط يجب تلبيتها ، قم بربطها معًا باستخدام && :

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

تصبح القواعد الكاملة:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

6. حذف المسودات وقراءتها: التحكم في الوصول المستند إلى السمات

مثلما يمكن للمؤلفين إنشاء المسودات وتحديثها ، يمكنهم أيضًا حذف المسودات.

resource.data.authorUID == request.auth.uid

بالإضافة إلى ذلك ، يُسمح للمؤلفين الذين لديهم سمة isModerator على رمز المصادقة الخاص بهم بحذف المسودات:

request.auth.token.isModerator == true

نظرًا لأن أيًا من هذين الشرطين كافيان للحذف ، قم بربطهما بعامل OR منطقي ، || :

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

تنطبق نفس الشروط على القراءات ، بحيث يمكن إضافة الإذن إلى القاعدة:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

القواعد الكاملة الآن هي:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر الآن.

7. يقرأ المنشورات وينشئها ويحذفها: تعطيل أنماط الوصول المختلفة

نظرًا لأن أنماط الوصول إلى المنشورات المنشورة ومسودات المنشورات مختلفة تمامًا ، فإن هذا التطبيق يلغي draft المنشورات في مجموعات منفصلة published . على سبيل المثال ، يمكن لأي شخص قراءة المنشورات المنشورة ولكن لا يمكن حذفها نهائيًا ، بينما يمكن حذف المسودات ولكن لا يمكن قراءتها إلا بواسطة المؤلف والمشرفين. في هذا التطبيق ، عندما يريد المستخدم نشر مسودة منشور مدونة ، يتم تشغيل وظيفة من شأنها إنشاء المنشور المنشور الجديد.

بعد ذلك ، ستكتب قواعد المنشورات المنشورة. أبسط قواعد الكتابة هي أنه يمكن لأي شخص قراءة المنشورات المنشورة ، ولا يمكن لأي شخص إنشاؤها أو حذفها. أضف هذه القواعد:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

بإضافة هذه إلى القواعد الحالية ، يصبح ملف القواعد بأكمله:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

8. تحديث المنشورات: الوظائف المخصصة والمتغيرات المحلية

شروط تحديث المنشور المنشور هي:

  • لا يمكن القيام به إلا من قبل المؤلف أو المشرف ، و
  • يجب أن يحتوي على جميع الحقول المطلوبة.

نظرًا لأنك كتبت بالفعل شروطًا لكونك مؤلفًا أو وسيطًا ، يمكنك نسخ الشروط ولصقها ، ولكن بمرور الوقت قد يصبح من الصعب قراءتها وصيانتها. بدلاً من ذلك ، ستنشئ وظيفة مخصصة تغلف منطق كونك إما مؤلفًا أو وسيطًا. بعد ذلك ، ستسميها من شروط متعددة.

قم بإنشاء وظيفة مخصصة

فوق بيان المطابقة للمسودات ، أنشئ وظيفة جديدة تسمى isAuthorOrModerator والتي تأخذ مستند المنشور كوسيطات (سيعمل هذا مع المسودات أو المنشورات المنشورة) وكائن المصادقة الخاص بالمستخدم:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

استخدم المتغيرات المحلية

داخل الوظيفة ، استخدم الكلمة الرئيسية let لتعيين متغيري isAuthor و isModerator . يجب أن تنتهي جميع الدوال ببيان return ، وستُرجع وظائفنا قيمة منطقية تشير إلى ما إذا كان أي متغير صحيحًا:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

استدعاء الوظيفة

ستقوم الآن بتحديث قاعدة المسودات لاستدعاء هذه الوظيفة ، مع الحرص على تمرير resource.data كأول وسيط:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

يمكنك الآن كتابة شرط لتحديث المنشورات المنشورة التي تستخدم أيضًا الوظيفة الجديدة:

allow update: if isAuthorOrModerator(resource.data, request.auth);

أضف عمليات التحقق

لا ينبغي تغيير بعض حقول المنشور المنشور ، خاصةً url و authorUID و publishedAt غير قابلة للتغيير. يجب أن يظل الحقلان الآخران ، title content ، visible موجودين بعد التحديث. أضف شروطًا لفرض هذه المتطلبات الخاصة بالتحديثات على المنشورات المنشورة:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

قم بإنشاء وظيفة مخصصة بنفسك

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

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

وملف القاعدة الكامل هو:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

أعد إجراء الاختبارات. في هذه المرحلة ، يجب أن يكون لديك 5 اختبارات ناجحة و 4 اختبارات فاشلة.

9. التعليقات: المجموعات الفرعية وأذونات مزود تسجيل الدخول

تسمح المنشورات المنشورة بالتعليقات ، ويتم تخزين التعليقات في مجموعة فرعية من المنشور المنشور ( /published/{postID}/comments/{commentID} ). افتراضيًا ، لا تنطبق قواعد المجموعة على المجموعات الفرعية. لا تريد تطبيق نفس القواعد التي تنطبق على المستند الأصلي للنشر المنشور على التعليقات ؛ سوف تصنع أشياء مختلفة.

لكتابة قواعد الوصول إلى التعليقات ، ابدأ ببيان المباراة:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

قراءة التعليقات: لا يمكن أن تكون مجهول الهوية

بالنسبة لهذا التطبيق ، يمكن فقط للمستخدمين الذين قاموا بإنشاء حساب دائم ، وليس حساب مجهول قراءة التعليقات. لفرض هذه القاعدة ، ابحث عن سمة sign_in_provider الموجودة في كل كائن auth.token :

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

تكوين التعليقات: التحقق من قائمة الرفض

هناك ثلاثة شروط لإنشاء تعليق:

  • يجب أن يكون لدى المستخدم بريد إلكتروني تم التحقق منه
  • يجب أن يكون التعليق أقل من 500 حرف ، و
  • لا يمكن أن يكونوا ضمن قائمة المستخدمين المحظورين ، والتي يتم تخزينها في متجر firestore في مجموعة bannedUsers . أخذ هذه الشروط واحدًا تلو الآخر:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

القاعدة الأخيرة لإنشاء التعليقات هي:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

ملف القواعد بأكمله هو الآن:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

10. تحديث التعليقات: القواعد المستندة إلى الوقت

منطق العمل الخاص بالتعليقات هو أنه يمكن تحريرها بواسطة مؤلف التعليق لمدة ساعة بعد الإنشاء. لتنفيذ ذلك ، استخدم الطابع الزمني createdAt .

أولاً ، لإثبات أن المستخدم هو المؤلف:

request.auth.uid == resource.data.authorUID

بعد ذلك ، تم إنشاء التعليق خلال الساعة الماضية:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

بدمجها مع عامل التشغيل المنطقي AND ، تصبح قاعدة تحديث التعليقات:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

11. حذف التعليقات: التحقق من ملكية الوالدين

يمكن حذف التعليقات بواسطة مؤلف التعليق أو الوسيط أو مؤلف منشور المدونة.

أولاً ، نظرًا لأن الوظيفة المساعدة التي أضفتها في وقت سابق عمليات التحقق من حقل authorUID الذي يمكن أن يكون موجودًا في منشور أو تعليق ، يمكنك إعادة استخدام وظيفة المساعد للتحقق مما إذا كان المستخدم هو المؤلف أو الوسيط:

isAuthorOrModerator(resource.data, request.auth)

للتحقق مما إذا كان المستخدم هو مؤلف منشور المدونة ، استخدم get على البحث عن المنشور في Firestore:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

نظرًا لأن أيًا من هذه الشروط كافٍ ، استخدم عامل التشغيل المنطقي OR بينهما:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

أعد إجراء الاختبارات وتأكد من اجتياز اختبار آخر.

وملف القواعد بأكمله هو:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 charachters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. الخطوات التالية

تهانينا! لقد كتبت قواعد الأمان التي نجحت في اجتياز جميع الاختبارات وحصلت على التطبيق!

فيما يلي بعض الموضوعات ذات الصلة للتعمق فيها بعد ذلك:

  • منشور مدونة : كيفية كتابة تعليمات برمجية لمراجعة قواعد الأمان
  • Codelab : المشي من خلال التطوير المحلي الأول مع المحاكيات
  • فيديو : كيفية استخدام إعداد CI للاختبارات القائمة على المحاكي باستخدام إجراءات GitHub