1. 准备工作
Cloud Firestore、Cloud Storage for Firebase 和 Realtime Database 依赖于您编写的配置文件来授予读写权限。该配置称为安全规则,也可以充当应用的架构。它是应用开发过程中最重要的部分之一。此 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
然后,进入 initial-state 目录,您将在本 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 或更高版本,但更高版本包含更多 bug 修复。
$ firebase --version 9.10.2
3. 运行测试
在本部分中,您将在本地运行测试。这意味着,现在可以启动 Emulator Suite 了。
启动模拟器
您将使用的应用有三个主要的 Firestore 集合:drafts
包含正在撰写的博文,published
集合包含已发布的博文,而 comments
是已发布博文的子集合。该代码库附带了安全规则的单元测试,这些安全规则定义了用户在 drafts
、published
和 comments
集合中创建、读取、更新和删除文档所需的用户属性和其他条件。您将编写安全规则,以使这些测试通过。
首先,您的数据库处于锁定状态:对数据库的读取和写入操作会被普遍拒绝,并且所有测试都会失败。在您编写安全规则时,测试会通过。如需查看测试,请在编辑器中打开 functions/test.js
。
在命令行中,使用 emulators:exec
启动模拟器并运行测试:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
滚动到输出的顶部:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
目前有 9 个失败。在构建规则文件时,您可以观察通过的测试数量,从而衡量进度。
4. 创建博文草稿。
由于草稿博文的访问权限与已发布博文的访问权限截然不同,因此此博客应用会将草稿博文存储在单独的集合 /drafts
中。只有作者或版主可以访问草稿,并且草稿会针对必需字段和不可变字段进行验证。
打开 firestore.rules
文件,您会看到一个默认规则文件:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
match 语句 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
由于所有这些条件都必须为 true,因此请使用逻辑 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
由于满足其中任一条件就足以进行删除,因此请使用逻辑“或”运算符 ||
将这两个条件串联起来:
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. 更新已发布的帖子:自定义函数和局部变量
更新已发布帖子的条件如下:
- 只能由作者或版主执行,并且
- 必须包含所有必填字段。
由于您已经为成为作者或版主编写了条件,因此可以复制并粘贴这些条件,但随着时间的推移,这些条件可能会变得难以阅读和维护。相反,您将创建一个自定义函数,用于封装作为作者或版主的逻辑。然后,您将从多个条件中调用它。
创建自定义函数
在草稿的 match 语句上方,创建一个名为 isAuthorOrModerator
的新函数,该函数将帖子文档(适用于草稿或已发布的帖子)和用户的身份验证对象作为实参:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
使用局部变量
在函数内,使用 let
关键字设置 isAuthor
和 isModerator
变量。所有函数都必须以 return 语句结尾,我们的函数将返回一个布尔值,指示任一变量是否为 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 /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 characters
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
接下来,评论是在过去 1 小时内创建的:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
将这些条件与逻辑 AND 运算符结合使用,更新评论的规则如下:
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
重新运行测试,并确保又有一个测试通过。
11. 删除评论:检查父级所有权
评论可由评论作者、管理员或博文作者删除。
首先,由于您之前添加的辅助函数会检查帖子或评论中可能存在的 authorUID
字段,因此您可以重复使用该辅助函数来检查用户是否为作者或版主:
isAuthorOrModerator(resource.data, request.auth)
如需检查用户是否为博文作者,请使用 get
在 Firestore 中查找相应博文:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
由于满足其中任何一个条件就足够了,因此请在这些条件之间使用逻辑 OR 运算符:
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
重新运行测试,并确保又有一个测试通过。
整个规则文件如下所示:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. 后续步骤
恭喜!您已编写了安全规则,使所有测试都通过了,并确保了应用的安全!
以下是一些相关主题,可供您接下来深入了解: