از داده های Firestore خود با قوانین امنیتی Firebase محافظت کنید

1. قبل از شروع

Cloud Firestore، Cloud Storage برای Firebase، و پایگاه داده بیدرنگ به فایل‌های پیکربندی که شما می‌نویسید برای اعطای دسترسی خواندن و نوشتن متکی هستند. این پیکربندی که قوانین امنیتی نام دارد، همچنین می تواند به عنوان نوعی طرحواره برای برنامه شما عمل کند. این یکی از مهمترین بخش های توسعه برنامه شما است. و این کد لبه شما را از طریق آن راهنمایی می کند.

پیش نیازها

کاری که خواهی کرد

در این کد لبه، شما یک پلت فرم وبلاگ ساده ساخته شده بر روی 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 که برای اجرای آزمایش‌ها استفاده می‌کنید بخشی از 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=**} ، از دستور ** برای اعمال بازگشتی به همه اسناد موجود در زیر مجموعه‌ها استفاده می‌کند. و از آنجایی که در سطح بالایی قرار دارد، در حال حاضر همان قانون کلی برای همه درخواست‌ها اعمال می‌شود، مهم نیست چه کسی درخواست را انجام می‌دهد یا چه داده‌هایی را می‌خواهد بخواند یا بنویسد.

با حذف عبارت inner-most match و جایگزینی آن با 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 استفاده کنید. همه توابع باید با یک عبارت 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 شروع کنید:

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. به روز رسانی نظرات: قوانین مبتنی بر زمان

منطق تجاری برای نظرات این است که آنها می توانند توسط نویسنده نظر برای یک ساعت پس از ایجاد ویرایش شوند. برای پیاده سازی این، از timestamp 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 Actions