1. 始める前に
Cloud Firestore、Cloud Storage for Firebase、Realtime Database は、読み取りと書き込みのアクセス権を付与するために作成する構成ファイルに依存しています。セキュリティ ルールと呼ばれるこの構成は、アプリのスキーマとしても機能します。これは、アプリケーション開発において最も重要な部分の 1 つです。この 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 に更新されます。
コメント
- 公開された投稿ではコメントが許可されており、コメントは公開された各投稿のサブコレクションです。
- 不正行為を減らすため、コメントを残すには、ユーザーが確認済みのメールアドレスを所有しており、拒否リストに登録されていない必要があります。
- コメントは投稿後 1 時間以内にのみ更新できます。
- コメントは、コメントの投稿者、元の投稿の投稿者、管理メンバーによって削除されることがあります。
アクセスルールの他に、必須フィールドとデータ検証を適用するセキュリティ ルールを作成します。
すべて Firebase Emulator Suite を使用してローカルで行われます。
ソースコードを取得する
この Codelab では、セキュリティ ルールのテストから始めますが、セキュリティ ルール自体は最小限です。そのため、まずソースを複製してテストを実行する必要があります。
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
次に、initial-state ディレクトリに移動します。この Codelab の残りの作業は、このディレクトリで行います。
$ cd codelab-rules/initial-state
次に、テストを実行できるように依存関係をインストールします。インターネット接続が遅い場合は、1 ~ 2 分ほどかかることがあります。
# 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. テストを実行する
このセクションでは、テストをローカルで実行します。Emulator Suite を起動するタイミングです。
エミュレータを起動する
このアプリケーションで使用する Firestore コレクションは 3 つあります。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
の 3 つが含まれている場合にのみ作成できます。(ユーザーは 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
更新の 2 つ目の要件は、2 つの属性 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
これらの条件のいずれか 1 つが削除に十分であるため、論理 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. 公開済みの投稿を更新する: カスタム関数とローカル変数
公開済みの投稿を更新する条件は次のとおりです。
- 投稿者またはモデレーターのみが実行できる。
- すべての必須フィールドが含まれている必要があります。
作成者またはモデレーターの条件はすでに記述されているため、条件をコピーして貼り付けることもできますが、時間が経つにつれて読み取りやメンテナンスが難しくなる可能性があります。代わりに、投稿者またはモデレーターのいずれかになるためのロジックをカプセル化するカスタム関数を作成します。次に、複数の条件から呼び出します。
カスタム関数を作成する
下書きの 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
の各フィールドは不変です。他の 2 つのフィールド 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";
テストを再実行し、もう 1 つのテストが合格することを確認します。
コメントの作成: 拒否リストの確認
コメントを作成するには、次の 3 つの条件があります。
- ユーザーは確認済みのメールアドレスを持っている必要があります
- コメントは 500 文字未満であること。
bannedUsers
コレクションの firestore に保存されている禁止ユーザーのリストに登録されていないこと。これらの条件を 1 つずつ見ていきましょう。
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));
}
}
}
テストを再実行し、もう 1 つのテストが合格することを確認します。
10. コメントの更新: 時間ベースのルール
コメントのビジネス ロジックでは、コメントの作成後 1 時間はコメントの投稿者が編集できます。これを実装するには、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');
テストを再実行し、もう 1 つのテストが合格することを確認します。
11. コメントの削除: 親の所有権を確認する
コメントは、コメントの投稿者、管理メンバー、ブログ投稿の投稿者が削除できます。
まず、先ほど追加したヘルパー関数は、投稿またはコメントに存在する可能性のある authorUID
フィールドを確認するため、このヘルパー関数を再利用して、ユーザーが投稿者またはモデレーターであるかどうかを確認できます。
isAuthorOrModerator(resource.data, request.auth)
ユーザーがブログ投稿の作成者であるかどうかを確認するには、get
を使用して Firestore で投稿を検索します。
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
これらの条件のいずれか 1 つを満たせば十分なので、条件の間に論理 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;
テストを再実行し、もう 1 つのテストが合格することを確認します。
ルールファイル全体は次のとおりです。
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. 次のステップ
これで完了です。すべてのテストに合格し、アプリケーションを保護するセキュリティ ルールを記述しました。
関連するトピックを以下に示します。