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 Functions がトリガーされ、
published
コレクションに新しいドキュメントが作成されます。 - 下書きは、作成者またはサイト管理者が削除できます
公開されたブログ投稿:
- 公開済みの投稿はユーザーが作成することはできず、関数を介してのみ作成できます。
- 削除(復元可能)のみが可能です。その場合、
visible
属性は false に更新されます。
コメント
- 公開済みの投稿では、公開された各投稿のサブコレクションであるコメントを使用できます。
- 不正行為を減らすため、ユーザーがコメントを投稿するには、確認済みのメールアドレスを持ち、拒否リストに登録されていない必要があります。
- コメントを更新できるのは、投稿後 1 時間以内です。
- コメントは、コメントの作成者、元の投稿の投稿者、またはモデレーターが削除できます。
アクセスルールに加えて、必須フィールドとデータ検証を適用するセキュリティ ルールを作成します。
すべてがローカルで、Firebase Emulator Suite を使用して行われます。
ソースコードを取得する
この Codelab では、セキュリティ ルールのテストから始めますが、セキュリティ ルール自体はよく似ています。そのため、最初にテストを実行するためにソースのクローンを作成する必要があります。
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
次に、この Codelab の残りの作業を行う initial-state ディレクトリに移動します。
$ 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 を起動します。
エミュレータを起動する
作業するアプリケーションには、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 ステートメント 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
これらの条件をすべて満たす必要があるため、これらの条件を 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 つ目の要件は、authorUID
と createdAt
の 2 つの属性を変更しないことです。
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
タイトルは半角 50 文字(全角 25 文字)以内にしてください。
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. 公開済み投稿の更新: カスタム関数とローカル変数
公開済みの投稿を更新する条件は次のとおりです。
- 作成者またはモデレーターのみが行うことができます。
- すべての必須フィールドを含める必要があります。
あなたはすでに作成者またはモデレーターとなる条件を書いたので、条件をコピーして貼り付けることもできますが、時間が経つと、読みやすさと保守が困難になる可能性があります。代わりに、作成者またはモデレーターとなるロジックをカプセル化するカスタム関数を作成します。次に、複数の条件から呼び出します。
カスタム関数を作成する
下書きの 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 ステートメントを記述します。
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 文字未満にする必要があります。また、
- Firestore の
bannedUsers
コレクションに保存される禁止ユーザーのリストには追加できません。これらの条件を 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
上記の条件のいずれかで十分であるため、条件を論理 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. 次のステップ
これで完了です。すべてのテストに合格し、アプリケーションを保護するセキュリティ ルールを作成できました。
次に取り上げる関連トピックは次のとおりです。