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
是已發布帖子的子集合。該存儲庫附帶了安全規則的單元測試,這些規則定義了用戶創建、閱讀、更新和刪除drafts
、 published
和comments
集合中的文檔所需的用戶屬性和其他條件。您將編寫安全規則以使這些測試通過。
首先,您的數據庫被鎖定:對數據庫的讀取和寫入都被普遍拒絕,並且所有測試都會失敗。當您編寫安全規則時,測試就會通過。要查看測試,請在編輯器中打開functions/test.js
。
在命令行上,使用emulators:exec
啟動模擬器並運行測試:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
滾動到輸出的頂部:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
目前有9次失敗。當您構建規則文件時,您可以通過觀察更多測試的通過情況來衡量進度。
4. 創建博客文章草稿。
由於草稿博客文章的訪問權限與已發布博客文章的訪問權限非常不同,因此此博客應用程序將草稿博客文章存儲在單獨的集合/drafts
中。草稿只能由作者或版主訪問,並且對必填字段和不可變字段進行驗證。
打開firestore.rules
文件,您會發現一個默認規則文件:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
匹配語句match /{document=**}
使用**
語法遞歸地應用於子集合中的所有文檔。由於它位於頂層,因此現在相同的一攬子規則適用於所有請求,無論是誰發出請求或他們嘗試讀取或寫入什麼數據。
首先刪除最裡面的 match 語句並將其替換為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
接下來,只有包含三個必填字段( 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
的新函數,該函數將帖子文檔(這適用於草稿或已發布的帖子)和用戶的 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 語句結尾,我們的函數將返回一個布爾值,指示任一變量是否為 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);
添加驗證
已發布帖子的某些字段不應更改,特別是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
閱讀評論:不能匿名
對於此應用程序,只有創建永久帳戶的用戶(而非匿名帳戶)才能閱讀評論。要強制執行該規則,請查找每個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. 後續步驟
恭喜!您已經編寫了安全規則,使所有測試都通過並保護應用程序!
以下是接下來要深入探討的一些相關主題: