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

۱. قبل از شروع

Cloud Firestore، Cloud Storage for Firebase و Realtime Database برای اعطای دسترسی خواندن و نوشتن به فایل‌های پیکربندی که شما می‌نویسید، متکی هستند. این پیکربندی که Security Rules نام دارد، می‌تواند به عنوان نوعی طرحواره برای برنامه شما نیز عمل کند. این یکی از مهم‌ترین بخش‌های توسعه برنامه شماست. و این codelab شما را در این مسیر راهنمایی خواهد کرد.

پیش‌نیازها

کاری که انجام خواهید داد

در این آزمایشگاه کد، شما یک پلتفرم وبلاگ ساده ساخته شده بر روی Firestore را ایمن خواهید کرد. شما از شبیه‌ساز Firestore برای اجرای تست‌های واحد در برابر قوانین امنیتی استفاده خواهید کرد و اطمینان حاصل خواهید کرد که قوانین دسترسی مورد نظر شما را مجاز یا غیرمجاز می‌دانند.

شما یاد خواهید گرفت که چگونه:

  • مجوزهای جزئی اعطا کنید
  • اعتبارسنجی داده‌ها و نوع‌ها را اعمال کنید
  • پیاده‌سازی کنترل دسترسی مبتنی بر ویژگی
  • اعطای دسترسی بر اساس روش احراز هویت
  • ایجاد توابع سفارشی
  • ایجاد قوانین امنیتی مبتنی بر زمان
  • پیاده‌سازی یک لیست انکار و حذف‌های نرم‌افزاری
  • بفهمید چه زمانی داده‌ها را برای برآورده کردن الگوهای دسترسی چندگانه، غیرنرمال‌سازی کنید

۲. راه‌اندازی

این یک برنامه وبلاگ نویسی است. در اینجا خلاصه ای از عملکرد برنامه آمده است:

پیش‌نویس پست‌های وبلاگ:

  • کاربران می‌توانند پست‌های وبلاگ پیش‌نویس ایجاد کنند که در مجموعه drafts قرار دارند.
  • نویسنده می‌تواند تا زمان آماده شدن پیش‌نویس برای انتشار، آن را به‌روزرسانی کند.
  • وقتی آماده انتشار شد، یک تابع Firebase فعال می‌شود که یک سند جدید در مجموعه published ایجاد می‌کند.
  • پیش‌نویس‌ها می‌توانند توسط نویسنده یا مدیران سایت حذف شوند.

پست‌های منتشر شده در وبلاگ:

  • پست‌های منتشر شده نمی‌توانند توسط کاربران ایجاد شوند، فقط از طریق یک تابع.
  • آنها فقط می‌توانند حذف موقت شوند، که یک ویژگی visible را به نادرست به‌روزرسانی می‌کند.

نظرات

  • پست‌های منتشر شده امکان ارسال نظر را فراهم می‌کنند که زیرمجموعه‌ای از هر پست منتشر شده است.
  • برای کاهش سوءاستفاده، کاربران باید یک آدرس ایمیل تأیید شده داشته باشند و برای ارسال نظر در لیست انکارکنندگان نباشند.
  • نظرات فقط تا یک ساعت پس از ارسال قابل به‌روزرسانی هستند.
  • نظرات می‌توانند توسط نویسنده نظر، نویسنده پست اصلی یا توسط مدیران حذف شوند.

علاوه بر قوانین دسترسی، شما قوانین امنیتی ایجاد خواهید کرد که فیلدهای الزامی و اعتبارسنجی داده‌ها را اعمال می‌کنند.

همه چیز به صورت محلی و با استفاده از Firebase Emulator Suite اتفاق خواهد افتاد.

دریافت کد منبع

در این آزمایشگاه کد، شما با تست‌هایی برای قوانین امنیتی شروع خواهید کرد، اما خود قوانین امنیتی حداقلی هستند، بنابراین اولین کاری که باید انجام دهید این است که منبع را برای اجرای تست‌ها کلون کنید:

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

سپس به دایرکتوری initial-state بروید، جایی که بقیه‌ی این آزمایشگاه کد را در آنجا انجام خواهید داد:

$ cd codelab-rules/initial-state

حالا، وابستگی‌ها را نصب کنید تا بتوانید تست‌ها را اجرا کنید. اگر اتصال اینترنت شما کند است، این ممکن است یک یا دو دقیقه طول بکشد:

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

دریافت رابط خط فرمان فایربیس

مجموعه شبیه‌ساز (Emulator Suite) که برای اجرای تست‌ها استفاده خواهید کرد، بخشی از Firebase CLI (رابط خط فرمان) است که می‌تواند با دستور زیر روی دستگاه شما نصب شود:

$ npm install -g firebase-tools

در مرحله بعد، تأیید کنید که آخرین نسخه CLI را دارید. این آزمایشگاه کد باید با نسخه ۸.۴.۰ یا بالاتر کار کند، اما نسخه‌های بعدی شامل رفع اشکالات بیشتری هستند.

$ firebase --version
9.10.2

۳. تست‌ها را اجرا کنید

در این بخش، تست‌ها را به صورت محلی اجرا خواهید کرد. این به این معنی است که زمان راه‌اندازی مجموعه شبیه‌ساز فرا رسیده است.

شروع شبیه‌سازها

برنامه‌ای که با آن کار خواهید کرد، سه مجموعه اصلی 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

...

در حال حاضر ۹ شکست وجود دارد. همزمان با ساخت فایل قوانین، می‌توانید با مشاهده‌ی موفقیت آزمایش‌های بیشتر، پیشرفت را اندازه‌گیری کنید.

۴. پیش‌نویس‌های پست وبلاگ را ایجاد کنید.

از آنجا که دسترسی به پست‌های وبلاگ پیش‌نویس با دسترسی به پست‌های وبلاگ منتشر شده بسیار متفاوت است، این برنامه وبلاگ‌نویسی، پست‌های وبلاگ پیش‌نویس را در یک مجموعه جداگانه، /drafts ذخیره می‌کند. پیش‌نویس‌ها فقط توسط نویسنده یا مدیر قابل دسترسی هستند و برای فیلدهای الزامی و تغییرناپذیر، اعتبارسنجی‌هایی دارد.

با باز کردن فایل firestore.rules ، یک فایل قوانین پیش‌فرض پیدا خواهید کرد:

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

دستور match، match /{document=**} ، از سینتکس ** برای اعمال بازگشتی به تمام اسناد موجود در زیرمجموعه‌ها استفاده می‌کند. و از آنجا که در سطح بالا قرار دارد، در حال حاضر همان قانون کلی برای همه درخواست‌ها اعمال می‌شود، صرف نظر از اینکه چه کسی درخواست را ارسال می‌کند یا چه داده‌هایی را سعی در خواندن یا نوشتن دارند.

با حذف عبارت 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"
])

آخرین الزام برای ایجاد یک پست وبلاگ این است که عنوان نمی‌تواند بیش از ۵۰ کاراکتر باشد:

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;
    }
  }
}

در ترمینال، تست‌ها را دوباره اجرا کنید و تأیید کنید که تست اول با موفقیت انجام می‌شود.

۵. پیش‌نویس‌های پست وبلاگ را به‌روزرسانی کنید.

در مرحله بعد، همزمان با اصلاح پیش‌نویس پست‌های وبلاگ توسط نویسندگان، اسناد پیش‌نویس نیز ویرایش خواهند شد. برای شرایطی که یک پست می‌تواند به‌روزرسانی شود، یک قانون ایجاد کنید. ابتدا، فقط نویسنده می‌تواند پیش‌نویس‌های خود را به‌روزرسانی کند. توجه داشته باشید که در اینجا UID که قبلاً نوشته شده است، resource.data.authorUID بررسی می‌کنید:

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

دومین الزام برای به‌روزرسانی این است که دو ویژگی authorUID و createdAt نباید تغییر کنند:

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

و در نهایت، عنوان باید ۵۰ کاراکتر یا کمتر باشد:

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;
    }
  }
}

تست‌های خود را دوباره اجرا کنید و تأیید کنید که تست دیگری با موفقیت انجام می‌شود.

۶. حذف و خواندن پیش‌نویس‌ها: کنترل دسترسی مبتنی بر ویژگی

همانطور که نویسندگان می‌توانند پیش‌نویس‌ها را ایجاد و به‌روزرسانی کنند، می‌توانند پیش‌نویس‌ها را نیز حذف کنند.

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;
    }
  }
}

تست‌های خود را دوباره اجرا کنید و تأیید کنید که تست دیگری اکنون با موفقیت انجام می‌شود.

۷. خواندن، ایجاد و حذف پست‌های منتشر شده: غیر نرمال‌سازی برای الگوهای دسترسی مختلف

از آنجا که الگوهای دسترسی برای پست‌های منتشر شده و پست‌های پیش‌نویس بسیار متفاوت هستند، این برنامه پست‌ها را به مجموعه‌های جداگانه 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;
    }
  }
}

تست‌ها را دوباره اجرا کنید و تأیید کنید که تست دیگری با موفقیت انجام می‌شود.

۸. به‌روزرسانی پست‌های منتشر شده: توابع سفارشی و متغیرهای محلی

شرایط لازم برای به‌روزرسانی یک پست منتشر شده عبارتند از:

  • این کار فقط توسط نویسنده یا مدیر قابل انجام است، و
  • باید شامل تمام فیلدهای مورد نیاز باشد.

از آنجایی که قبلاً شرایط مربوط به نویسنده یا مدیر بودن را نوشته‌اید، می‌توانید شرایط را کپی و پیست کنید، اما با گذشت زمان خواندن و نگهداری آن دشوار می‌شود. در عوض، یک تابع سفارشی ایجاد خواهید کرد که منطق مربوط به نویسنده یا مدیر بودن را در خود جایگذاری می‌کند. سپس، آن را از چندین شرط فراخوانی خواهید کرد.

ایجاد یک تابع سفارشی

بالای عبارت match برای drafts، یک تابع جدید به نام isAuthorOrModerator ایجاد کنید که یک سند post (این برای drafts یا پست‌های منتشر شده کار می‌کند) و شیء auth کاربر را به عنوان آرگومان دریافت می‌کند:

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;
}

تابع را فراخوانی کنید

اکنون قانون فراخوانی آن تابع توسط drafts را به‌روزرسانی خواهید کرد، و مراقب باشید که 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"
])

یک تابع سفارشی به تنهایی ایجاد کنید

و در نهایت، شرطی اضافه کنید که عنوان کمتر از ۵۰ کاراکتر باشد. از آنجا که این منطق دوباره استفاده شده است، می‌توانید این کار را با ایجاد یک تابع جدید، 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);
    }
  }
}

تست‌ها را دوباره اجرا کنید. در این مرحله، باید ۵ تست موفق و ۴ تست ناموفق داشته باشید.

۹. نظرات: زیرمجموعه‌ها و مجوزهای ورود به سیستم ارائه‌دهنده

پست‌های منتشر شده امکان درج نظر را دارند و نظرات در زیرمجموعه‌ای از پست منتشر شده ( /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";

تست‌های خود را دوباره اجرا کنید و تأیید کنید که یک تست دیگر با موفقیت انجام می‌شود.

ایجاد نظرات: بررسی لیست نظرات رد شده

برای ایجاد نظر سه شرط وجود دارد:

  • کاربر باید یک ایمیل تأیید شده داشته باشد
  • نظر باید کمتر از ۵۰۰ کاراکتر باشد، و
  • آنها نمی‌توانند در لیست کاربران ممنوعه باشند، که در 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));
    }
  }
}

تست‌ها را دوباره اجرا کنید و مطمئن شوید که یک تست دیگر با موفقیت انجام می‌شود.

۱۰. به‌روزرسانی نظرات: قوانین مبتنی بر زمان

منطق تجاری نظرات این است که می‌توانند توسط نویسنده نظر تا یک ساعت پس از ایجاد، ویرایش شوند. برای پیاده‌سازی این، از برچسب زمانی 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');

تست‌ها را دوباره اجرا کنید و مطمئن شوید که یک تست دیگر با موفقیت انجام می‌شود.

۱۱. حذف نظرات: بررسی مالکیت والد

نظرات می‌توانند توسط نویسنده نظر، مدیر یا نویسنده پست وبلاگ حذف شوند.

اولاً، از آنجایی که تابع کمکی که قبلاً اضافه کردید، فیلد 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;
    }
  }
}

۱۲. مراحل بعدی

تبریک! شما قوانین امنیتی را نوشتید که باعث شد همه تست‌ها با موفقیت انجام شوند و برنامه امن شود!

در اینجا چند موضوع مرتبط برای بررسی بیشتر آورده شده است:

  • پست وبلاگ : نحوه بررسی کد، قوانین امنیتی
  • Codelab : قدم زدن در توسعه اولیه محلی با شبیه‌سازها
  • ویدیو : نحوه استفاده از Setup CI برای تست‌های مبتنی بر شبیه‌ساز با استفاده از GitHub Actions