قم بحماية بيانات 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
    }
  }
}

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

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

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

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

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 . يجب أن تنتهي جميع الدوال ببيان إرجاع، وستُرجع دالتنا قيمة منطقية تشير إلى ما إذا كان أي من المتغيرين صحيحًا:

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 characters
        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 characters
        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