使用 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 Emulator Suite。

獲取源代碼

在此 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

您將用於運行測試的 Emulator Suite 是 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 語句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. 更新發布的帖子:自定義函數和局部變量

更新已發布帖子的條件是:

  • 它只能由作者或版主完成,並且
  • 它必須包含所有必填字段。

由於您已經編寫了成為作者或版主的條件,您可以復制和粘貼條件,但隨著時間的推移,可能會變得難以閱讀和維護。相反,您將創建一個自定義函數來封裝作為作者或版主的邏輯。然後,您將從多個條件中調用它。

創建自定義函數

在草稿的 match 語句上方,創建一個名為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 語句結尾,我們的函數將返回一個布爾值,指示任一變量是否為真:

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字段是不可變的。其他兩個字段titlecontentvisible必須在更新後仍然存在。添加條件以強制執行這些更新已發布帖子的要求:

// 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 個字符,並且
  • 他們不能出現在被禁止用戶列表中,該列表存儲在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 進行基於模擬器的測試