使用 Firebase 安全規則保護您的 Firestore 數據

1. 開始之前

Cloud Firestore、Cloud Storage for Firebase 和實時數據庫依賴您編寫的配置文件來授予讀寫訪問權限。該配置稱為安全規則,也可以充當應用程序的一種架構。這是開發應用程序最重要的部分之一。此 Codelab 將引導您完成它。

先決條件

  • 簡單的編輯器,例如 Visual Studio Code、Atom 或 Sublime Text
  • Node.js 8.6.0 或更高版本(要安裝 Node.js,請使用 nvm ;要檢查您的版本,請運行node --version
  • Java 7 或更高版本(要安裝 Java,請使用這些說明;要檢查您的版本,請運行java -version

你會做什麼

在此 Codelab 中,您將保護一個基於 Firestore 構建的簡單博客平台。您將使用 Firestore 模擬器根據安全規則運行單元測試,並確保規則允許和禁止您期望的訪問。

您將學習如何:

  • 授予細化權限
  • 強制執行數據和類型驗證
  • 實施基於屬性的訪問控制
  • 根據身份驗證方法授予訪問權限
  • 創建自定義函數
  • 創建基於時間的安全規則
  • 實施拒絕列表和軟刪除
  • 了解何時對數據進行非規範化以滿足多種訪問模式

2. 設置

這是一個博客應用程序。以下是應用程序功能的高級摘要:

博客文章草稿:

  • 用戶可以創建草稿博客文章,這些草稿位於drafts集合中。
  • 作者可以繼續更新草稿,直到準備好發佈為止。
  • 當準備好發佈時,會觸發 Firebase 函數,在published集合中創建一個新文檔。
  • 草稿可以由作者或網站管理員刪除

發表的博客文章:

  • 用戶無法創建已發布的帖子,只能通過函數創建。
  • 它們只能被軟刪除,這會將visible屬性更新為 false。

評論

  • 已發布的帖子允許評論,評論是每個已發布帖子的子集合。
  • 為了減少濫用,用戶必須擁有經過驗證的電子郵件地址,並且不能否認才能發表評論。
  • 評論只能在發布後一小時內更新。
  • 評論可以由評論作者、原始帖子的作者或版主刪除。

除了訪問規則之外,您還將創建強制執行必填字段和數據驗證的安全規則。

一切都將使用 Firebase 模擬器套件在本地進行。

獲取源代碼

在此 Codelab 中,您將從安全規則測試開始,但安全規則本身是最小的,因此您需要做的第一件事是克隆源以運行測試:

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

然後進入初始狀態目錄,您將在其中完成本 Codelab 的剩餘部分:

$ cd codelab-rules/initial-state

現在,安裝依賴項以便您可以運行測試。如果您的互聯網連接速度較慢,這可能需要一兩分鐘:

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

獲取 Firebase CLI

您將用於運行測試的模擬器套件是 Firebase CLI(命令行界面)的一部分,可以使用以下命令將其安裝在您的計算機上:

$ npm install -g firebase-tools

接下來,確認您擁有最新版本的 CLI。此 Codelab 應適用於 8.4.0 或更高版本,但更高版本包含更多錯誤修復。

$ firebase --version
9.10.2

3. 運行測試

在本部分中,您將在本地運行測試。這意味著是時候啟動模擬器套件了。

啟動模擬器

您將使用的應用程序具有三個主要 Firestore 集合: drafts包含正在進行的博客文章, published集合包含已發布的博客文章, comments是已發布帖子的子集合。該存儲庫附帶了安全規則的單元測試,這些規則定義了用戶創建、閱讀、更新和刪除draftspublishedcomments集合中的文檔所需的用戶屬性和其他條件。您將編寫安全規則以使這些測試通過。

首先,您的數據庫被鎖定:對數據庫的讀取和寫入都被普遍拒絕,並且所有測試都會失敗。當您編寫安全規則時,測試就會通過。要查看測試,請在編輯器中打開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 語句並將其替換為match /drafts/{draftID} 。 (文檔結構的註釋對規則很有幫助,並將包含在此 Codelab 中;它們始終是可選的。)

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

接下來,只有包含三個必填字段( authorUIDcreatedAttitle的文檔才能創建。 (用戶不提供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

更新的第二個要求是兩個屬性, authorUIDcreatedAt不應更改:

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. 讀取、創建和刪除已發布的帖子:針對不同的訪問模式進行非規範化

由於已發布帖子和草稿帖子的訪問模式非常不同,因此此應用程序將帖子非規範化為單獨的draftpublished集合。例如,已發布的帖子任何人都可以閱讀,但不能硬刪除,而草稿可以刪除,但只能由作者和版主閱讀。在此應用程序中,當用戶想要發布草稿博客文章時,會觸發一個函數來創建新的已發布文章。

接下來,您將為已發布的帖子編寫規則。最簡單的規則是任何人都可以閱讀已發布的帖子,並且任何人都不能創建或刪除。添加這些規則:

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的新函數,該函數將帖子文檔(這適用於草稿或已發布的帖子)和用戶的 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關鍵字設置isAuthorisModerator變量。所有函數都必須以 return 語句結尾,我們的函數將返回一個布爾值,指示任一變量是否為 true:

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

添加驗證

已發布帖子的某些字段不應更改,特別是urlauthorUIDpublishedAt字段是不可變的。其他兩個字段( titlecontent以及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

閱讀評論:不能匿名

對於此應用程序,只有創建永久帳戶的用戶(而非匿名帳戶)才能閱讀評論。要強制執行該規則,請查找每個auth.token對像上的sign_in_provider屬性:

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

重新運行測試,並確認另一項測試通過。

創建評論:檢查拒絕列表

創建評論需要滿足三個條件:

  • 用戶必須擁有經過驗證的電子郵件
  • 評論必須少於 500 個字符,並且
  • 他們不能出現在禁止用戶列表中,該列表存儲在 Firestore 的bannedUsers集合中。一次考慮這些條件:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

創建評論的最終規則是:

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

整個規則文件現在是:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

重新運行測試,並確保又一項測試通過。

10.更新評論:基於時間的規則

評論的業務邏輯是評論作者可以在創建後一小時內對其進行編輯。要實現此目的,請使用createdAt時間戳。

首先,確定用戶是作者:

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

接下來,該評論是在過去一小時內創建的:

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

將它們與邏輯 AND 運算符結合起來,更新註釋的規則變為:

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

重新運行測試,並確保又一項測試通過。

11.刪除評論:檢查父所有權

評論可以由評論作者、版主或博客文章的作者刪除。

首先,由於您之前添加的輔助函數會檢查帖子或評論中可能存在的authorUID字段,因此您可以重用該輔助函數來檢查用戶是否是作者或版主:

isAuthorOrModerator(resource.data, request.auth)

要檢查用戶是否是博客文章作者,請使用get在 Firestore 中查找帖子:

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

由於這些條件中的任何一個都足夠,因此在它們之間使用邏輯 OR 運算符:

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

重新運行測試,並確保又一項測試通過。

整個規則文件是:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12. 後續步驟

恭喜!您已經編寫了安全規則,使所有測試都通過並保護應用程序!

以下是接下來要深入探討的一些相關主題:

  • 博客文章:如何進行代碼審查安全規則
  • Codelab :使用模擬器進行本地首次開發
  • 視頻:如何使用 GitHub Actions 設置 CI 進行基於模擬器的測試