
テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第1回】
2021.12.20
テスト駆動で成績データコレクションのルールを実装する【第1回】
この記事では、全4回に分けて、Firestore セキュリティルールでのカスタム関数の利用について解説していきます。
成績評価システムを想定した、成績データコレクション records のルール実装を例に、解説を進めていきます。
なお、今回の「カスタム関数編」では、これまでの「テスト駆動で学ぶ Firestore セキュリティルール」シリーズの内容を前提としていますので、以下の記事をご参照ください。
データ検証編はこちら
「データ検証編」では、リクエストデータ、データベース上のデータの参照などについて解説
データ比較編はこちら
「データ比較編」では、データの比較や型チェックなどについて解説
カスタム関数編の簡単な流れ
「カスタム関数編」では、以下のような流れで解説を進めていきます。
- 第1回 : カスタム関数の簡単な解説と実装ルールの要件設定&前準備
- 第2回 : データ追加用のルールの作成
- 第3回 : データ更新用のルールの作成
- 第4回 : データ取得・削除用のルールの作成
今回の内容は?
第1回である今回は、最初に「データ比較編」で作成したセキュリティルールをカスタム関数を使って整理しながら、カスタム関数について簡単に解説します。
その後、この記事から始まる「カスタム関数編」の全4回を通して、ルールを実装する成績データコレクション records の要件を設定します。
テストで必要となるコレクション users、 seasons の要件も併せて設定します。
続いて、ルール実装の前準備として、テスト環境とテストデータを準備します。
カスタム関数について
まずは、前回の「データ比較編」で実装したルールを例に、カスタム関数について簡単に解説したいと思います。
「データ比較編」では、書店のネットショッピングサービスを例に、書籍コレクション books のルールを実装しました。
そのとき実装したルールは、以下の通りです。
「データ比較編」で実装したルール
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /books/{id}{ allow create: if request.auth.uid in request.resource.data.adminUsers && request.resource.data.size() == 9 && request.resource.data.title is string && request.resource.data.description is string && request.resource.data.releaseDate is timestamp && request.resource.data.price is int && request.resource.data.price >= 0 && request.resource.data.stock is int && request.resource.data.stock >= 0 && request.resource.data.condition in ["new","used"]; allow update: if request.auth.uid in resource.data.adminUsers && request.resource.data.size() == 9 && request.resource.data.title is string && request.resource.data.description is string && request.resource.data.releaseDate is timestamp && request.resource.data.price is int && request.resource.data.price >= 0 && request.resource.data.stock is int && request.resource.data.stock >= 0 && request.resource.data.condition in ["new","used"]; allow get: if resource.data.draft != true && resource.data.stock > 5; } } } |
get アクセスについては、「下書きデータでなく、かつ在庫が6以上」であれば取得が可能となっています。
create、 update アクセスは、大雑把に、どちらも書籍の管理担当者チェックと、書籍データのフォーマットチェックを実行。
書籍の管理担当者であり、かつリクエストデータが適切なフォーマットであれば、データの追加・更新が可能です。
ルール全体の見通しが悪くなっているため調整する
フォーマットチェックの内容が多く、これにより「allow式」が長くなってしまっているため、ルール全体の見通しが悪くなっています。
今回の記事のテーマとなるカスタム関数を使って、上記のルールを整理し、見通しの良いルールに調整したいと思います。
上述のルールは、カスタム関数を使って、以下のように書き換えることが可能です。
「データ比較編」で実装したルールをカスタム関数で書き替える
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /books/{id}{ // 書籍の管理担当者チェック関数 function isAdminUser(_resource){ return request.auth.uid in _resource.data.adminUsers; } // 書籍データのフォーマットチェック関数 function validBookData(){ return request.resource.data.size() == 9 && request.resource.data.title is string && request.resource.data.description is string && request.resource.data.releaseDate is timestamp && request.resource.data.price is int && request.resource.data.price >= 0 && request.resource.data.stock is int && request.resource.data.stock >= 0 && request.resource.data.condition in ["new","used"]; } allow create: if isAdminUser(request.resource) && validBookData(); allow update: if isAdminUser(resource) && validBookData(); allow get: if resource.data.draft != true && resource.data.stock > 5; } } } |
ここで、カスタム関数 isAdminUser() を定義して、書籍の管理担当者チェック部分を関数化しています。
create アクセスでは、リクエストデータの、 update アクセスでは、データベース上のデータの書籍管理担当者をチェックするため、参照するデータが異なります。
リクエストデータ、データベース上のデータ、どちらのデータのチェックにも対応できるように、チェック対象のデータリソースを isAdminUser() 関数の引数で指定できるように定義。
また、 validBookData() 関数を定義して、リクエストデータのフォーマットチェックを1つにまとめています。
カスタム関数でのルール整理で見通しがよくなった
カスタム関数によるルールの整理によって、「allow式」が短くなって、ルールの見通しがよくなりました。
ここでの解説と同様、「カスタム関数編」では、成績評価システムを想定した成績コレクション records のセキュリティルールを例に、カスタム関数を使ってルールを整理しながらセキュリティルールの実装を進めていきます。
成績データコレクションの要件(実装目標)を設定
まずは、成績コレクション records と、テストで必要となる users コレクション、 seasons コレクションの要件を設定します。
成績コレクション records の要件は、以下の通りに設定します。
コレクションパス
students/{studentId}/records
ロール
admin | システム管理者 |
teacher | 教師 |
student | 生徒 |
record ドキュメントのフォーマット
データ名称 | 内容 | データ型 | 条件 |
id | 成績ID | string型 | 必須。変更不可。 |
studentId | 生徒ID | string型 | 必須。変更不可。 |
season | 年度と学期 | map型 | 必須。変更不可。 |
name | 生徒の名前 | string型 | 必須 |
homeroomTeacher | 担任 | string型 | 必須 |
record | 成績 | map型 | なし |
各処理共通の要件
- ログインしていないユーザはアクセス不可
データ追加要件
- admin ユーザ以外は追加不可
- 成績フィールドは空のマップ
- 所定のフォーマットでないデータは追加不可
成績評価の設定は、teacher ユーザがデータ更新でのみ設定可能とします。
データ追加時に成績が設定されないように、追加時の成績フィールドは、空のマップのみ設定可能としています。
データ更新要件
admin ユーザの更新要件
- id の更新不可
- studentId の更新不可
- season の更新不可
- record の更新不可
teacher ユーザの更新要件
- 担任でないデータは更新不可
- 成績以外のフィールドは更新不可
- 対象シーズンが成績評価期間外の場合は更新不可
その他の更新要件
- student ユーザは更新不可
- 所定のフォーマットでないデータは更新不可
データ取得要件
- admin ユーザはデータの取得可
teacher ユーザの取得要件
- 担任でないデータは取得不可
student ユーザの取得要件
- 自分以外のデータは取得不可
- 対象シーズンが成績評価期間の場合は取得不可
データ削除要件
- admin ユーザ以外は削除不可
関連コレクションの要件を設定
以下、テストの中で records コレクション以外に必要となるコレクションの要件を簡単に設定します。
「seasons」コレクションのフォーマット
データ名称 | 内容 | データ型 | 条件 |
id | シーズンID。 {year}_{semester} のようなフォーマットで設定。 | string型 | 必須。変更不可。 |
evaluationPeriod | 成績評価期間 | bool型 | 必須 |
成績評価期間の更新は、admin ユーザだけが行えるものと想定します。
前述の records コレクションの要件設定でふれた通り、評価期間中のみ teacher ユーザが担当生徒の成績評価を更新できるものとします。
「users」コレクションのフォーマット
データ名称 | 内容 | データ型 | 条件 |
id | ユーザID | string型 | 必須。変更不可。 |
roles | ロール。 admin、 teacherまたは studentを設定。 | list型 | 必須 |
roles は、複数ロールを設定する場合を想定して「list型」としています。
例えば、 admin と teacher ロールを兼任する場合や、他のロールを追加する場合などを想定しています。
seasons、 users コレクションは、要件の設定のみで、本記事では、ルールの実装・解説はしません。
前準備 ~ テスト環境 ~
以下のリポジトリに、本記事のコードをまとめてあります。
【GitHub】
rightcode/firestore-security-rules-test_custom-function
テスト環境のセットアップ
以下のコマンドを実行して、テスト環境をセットアップしてください。
1 2 3 | git clone "https://github.com/rightcode/firestore-security-rules-test_custom-function" sandbox cd sandbox git checkout refs/tags/test-environment |
Firebase CLI ツールのインストール
Firebase CLI ツールをインストールしていない場合は、以下のコマンドを実行してインストールしてください。
1 | npm install -g firebase-tools |
npm パッケージのインストール
以下のコマンドを実行して、必要となる npm パッケージをインストールしてください。
1 | npm install |
Firestore エミュレータを起動
以下のコマンドを実行して、Firestore エミュレータを起動してください。
1 2 | firebase setup:emulators:firestore firebase emulators:start --only firestore |
以上で、テスト環境の準備は完了です。
テスト環境のセットアップの解説
テスト環境のセットアップの、より詳細な内容は、以下の記事をご参照ください。
master ブランチに実装済みのコードを設置してありますので、本記事中のコードを確認する場合は、以下のコマンドよりmaster ブランチをチェックアウトしてください。
1 | git checkout master |
※ コードの調整などにより、本記事の内容とは記述が若干異なる場合があります。
前準備 ~ テストデータ ~
テストを作成する前に、まず、テストデータ用のコードを用意します。
以下の4つファイルを作成してください。
- tests/data/collections/records.ts
- tests/data/collections/seasons.ts
- tests/data/collections/users.ts
- tests/data/InitialData.ts
追加したファイルに、コードを追加していきます。
成績データコレクションのテストデータ
tests/data/collections/records.ts に、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import { users } from "./users"; import { seasons } from "./seasons"; export namespace records { export namespace sample { // 成績ID export const id = "YYYYYYYYYY"; // 成績 export const record = { Math: 4, NationalLanguage: 3 }; } // コレクションパス export const collectionPath = "students/" + users.sample.student + "/records"; // 初期データ export const initialData = { id: sample.id, studentId: users.sample.student, season: seasons.sample.season, name: users.sample.student, homeroomTeacher: users.sample.teacher, record: {} }; // 更新用データ/システム管理者向け export const validUpdateDataForAdmin = { id: sample.id, studentId: users.sample.student, season: seasons.sample.season, name: "Eto", homeroomTeacher: "Fujita0000", record: {} }; // 更新用データ/教師向け export const validUpdateDataForTeacher = { id: sample.id, studentId: users.sample.student, season: seasons.sample.season, name: users.sample.student, homeroomTeacher: users.sample.teacher, record: { Math: 5, NationalLanguage: 5 } }; } |
「seasons」コレクションのテストデータ
tests/data/collections/seasons.ts に、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | export namespace seasons { export namespace sample { // 対象シーズン export const season = "2010_2" } // コレクションパス export const collectionPath = "seasons"; // 初期データ export const initialData = (evaluationPeriod: boolean) => ({ id: sample.season, evaluationPeriod: evaluationPeriod }); } |
「users」コレクションのテストデータ
tests/data/collections/users.ts に、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | export namespace users { export namespace sample { // システム管理者 export const admin = "Aida0000"; // 教師 export const teacher = "Baba0000"; // 生徒 export const student = "Dojima0000"; // その他の生徒 export const other_student = "Fujita" } // コレクションパス export const collectionPath = "users"; // 初期データ const _initialData: any = {}; _initialData[sample.admin] = { id: sample.admin, roles: ["admin"] }; _initialData[sample.teacher] = { id: sample.teacher, roles: ["teacher"] }; _initialData[sample.student] = { id: sample.student, roles: ["student"] }; _initialData[sample.other_student] = { id: sample.other_student, roles: ["student"] }; export const initialData = _initialData; } |
初期データのテストデータ
tests/data/InitialData.ts に、以下のコードを追加してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | import { FirestoreTestSupporter } from "firestore-test-supporter"; import * as firebase from "@firebase/testing"; import { users } from "./collections/users"; import { records } from "./collections/records"; import { seasons } from "./collections/seasons"; export default class InitialData { private supporter: FirestoreTestSupporter; private db: firebase.firestore.Firestore; constructor(rulesFilePath: string) { this.supporter = new FirestoreTestSupporter("my-test-project", rulesFilePath); this.db = this.supporter.getAdminFirestore(); } // usersコレクションに初期データを追加 async setupUsers() { for (const userId of Object.keys(users.initialData)) { const userDoc = this.db.collection(users.collectionPath).doc(userId); await firebase.assertSucceeds(userDoc.set(users.initialData[userId])) } } // recordsコレクションに初期データを追加 async setupRecords() { const recordDoc = this.db.collection(records.collectionPath).doc(records.initialData.id); await firebase.assertSucceeds(recordDoc.set(records.initialData)) } // seasonsコレクションに初期データを追加 async setupSeasons() { const seasonDoc = this.db.collection(seasons.collectionPath).doc(seasons.initialData(true).id); await firebase.assertSucceeds(seasonDoc.set(seasons.initialData(true))) } } |
テストデータの概要
tests/data /collections/records.ts
records コレクションのサンプルデータを定義しています。
初期データ、更新用データには records.ts、 seasons.ts、 users.ts で定義しているテスト用のサンプルデータを使用しています。
tests/data/collections/seasons.ts
seasons コレクションのサンプルデータを定義しています。
初期データには、引数により成績評価期間フラグの設定が可能です。
tests/data/collections/users.ts
users コレクションのサンプルデータを定義しています。
admin, teacher, student ロールのサンプルユーザを定義しています。
tests/data/InitialData.ts
初期データのセットアップ用クラス。
各テストの前に必要な初期データをデータベースに追加します。
第2回へつづく!
「カスタム関数編」第1回となるこの記事では、前回の記事「データ比較編」で作成したセキュリティルールを例に、カスタム関数の使い方を簡単に解説してみました。
また、カスタム関数の利用例として扱う、成績データコレクション records の要件を設定し、ルール実装に使用するテスト環境とテストデータを準備しました。
次回は、今回設定した要件に沿って、データ追加用ルールの実装を進めていきます。
お楽しみに!
第2回の記事はこちら
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン