テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】
IT技術
テスト駆動で商品データのルールを実装する
この記事では、セキュリティルールによるデータ検証について、商品データのルールを例に解説していきます。
前編・後編の二回に分け、テスト駆動で商品データのルールの実装を進めます。
条件設定で使用できるデータなどについても解説していたいと思います!
簡単な流れ
以下のような流れで解説を進めていきます。
前編
- 商品データコレクションの要件を設定
- テスト環境の準備
- テストデータ用のクラスを作成
- データ追加ルールを実装・解説
後編
- データ更新ルールを実装・解説
- データ取得ルールを実装・解説
- 全体テストで要件の最終チェック
- まとめ
今回は、前編パートの、「実装目標(要件)の設定からデータ追加ルールの実装まで」を解説していきます。
実装目標
商品データコレクション items を想定して、以下の要件を目標に実装を進めていきます。
商品データ追加の要件
- データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可
商品データ更新の要件
- データ更新をリクエストしたユーザが商品管理者と一致しない場合は追加不可
- 商品管理者の変更不可
- ロックされたデータの更新不可
- 商品名の変更不可
商品データ取得の要件
- 認証されていないユーザはデータ取得不可
- 売り切れ商品のデータ取得不可
前準備 ~ テスト環境の準備 ~
初めに、テスト環境を準備します。
テスト環境のセットアップ
以下のコマンドを実行して、テスト環境をセットアップします。
1git clone "https://github.com/rightcode/firestore-security-rules-test_data-verification.git" sandbox
2
3cd sandbox
4git checkout refs/tags/test-environment-only
上記のリポジトリに、この記事のコードがまとめてあります。
npm パッケージのインストール
以下のコマンドを実行して、npm パッケージをインストールしてください。
1npm install
Firebase CLI ツールのインストール
まだ、Firebase CLI ツールをインストールしていない場合は、以下のコマンドを実行してインストールします。
1npm install -g firebase-tools
Firestore エミュレータを起動
以下のコマンドを実行して、Firestore エミュレータを起動してください。
1firebase setup:emulators:firestore
2firebase emulators:start --only firestore
以上で、テスト環境の準備は完了です。
コードを確認したい場合は、以下のコマンドより、コーディング済みのタグを指定してチェックアウトしてください。
1// テスト実装済み
2git checkout refs/tags/test-only
3
4// テストとルール実装済み
5git checkout refs/tags/test-and-rules
テスト環境のセットアップの解説
テスト環境のセットアップについて、より詳細な内容は、以下の記事をご参照ください。
前準備 ~ テストデータクラス ~
テストを作成する前に、まず、テストデータ用のクラスを作成します。
tests/TestData.ts ファイルを作成して、以下の内容を記述してください。
テストデータクラスのコード
1class TestData {
2 // コレクションパスの取得
3 static getCollectionPath() {
4 return "items"
5 }
6
7 // 商品IDの取得
8 static getItemId() {
9 return "XXXXXXXXXX"
10 }
11
12 // 商品タイトルの取得
13 static getTitle() {
14 return "吾輩は犬である!"
15 }
16
17 // 商品管理者の取得
18 static getAdminUser() {
19 return "fuyutsuki";
20 }
21
22 // 初期データの取得
23 static getInitialData() {
24 return {
25 item_id: this.getItemId(),
26 title: this.getTitle(),
27 admin_user: this.getAdminUser(),
28 price: 1000,
29 description: "猫じゃないよ。犬だよ。",
30 locked: false,
31 sold_out: false
32 }
33 }
34
35 // 要件に沿った更新データの取得
36 static getValidUpdateData() {
37 return {
38 item_id: this.getItemId(),
39 title: this.getTitle(),
40 admin_user: this.getAdminUser(),
41 price: 12000,
42 description: "猫が好きです。でも犬はも~っと好きです。",
43 locked: false,
44 sold_out: false
45 };
46 }
47}
48
49export default TestData;
テストデータクラスの概要
getInitialData()
初期データの取得メソッド。
初期データの追加テストや、データの更新・取得テストの初期データの準備に使用します。
getValidUpdateData()
要件にあった更新データの取得メソッド。
データ更新テストに使用します。
商品データ追加ルールの実装
テスト作成の前に、データ追加の要件を再確認します。
- データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可
上記の要件に沿って、テストを作成します。
tests/data.add.test.ts ファイルを追加して、以下のようにコードを記述してください。
テストのコード
1// Firestoreエミュレータのホストとポートを指定
2process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
3
4import {FirestoreTestSupporter} from "firestore-test-supporter";
5import * as firebase from "@firebase/testing";
6import path from "path";
7
8// テストデータクラスの読み込み
9import TestData from "./TestData";
10
11describe("データ追加テスト", () => {
12 const supporter = new FirestoreTestSupporter("my-test-project", path.join(__dirname, "firestore.rules"));
13
14 // テストデータを変数に設定
15 const collectionPath = TestData.getCollectionPath();
16 const item_id = TestData.getItemId();
17 const initialData = TestData.getInitialData();
18 const admin_user = TestData.getAdminUser();
19
20 beforeEach(async () => {
21 // セキュリティルールの読み込み
22 await supporter.loadRules();
23 });
24
25 afterEach(async () => {
26 // データのクリーンアップ
27 await supporter.cleanup();
28 });
29
30 test('要件にあったデータの追加に成功', async () => {
31 const db = supporter.getFirestoreWithAuth(admin_user);
32 const doc = db.collection(collectionPath).doc(item_id);
33 await firebase.assertSucceeds(doc.set(initialData))
34 });
35
36 test('データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可', async () => {
37 // 商品管理者と異なるユーザで認証
38 const db = supporter.getFirestoreWithAuth("other_user");
39
40 const doc = db.collection(collectionPath).doc(item_id);
41 await firebase.assertFails(doc.set(initialData))
42 });
43});
テストの概要
コードの冒頭
Firestore エミュレータのホスト指定をし、作成したテストデータクラスの読み込みを行っています。
デフォルトポートでエミュレータを起動している場合は、ホスト指定は必要ありません。
ですが、今回は 58080 ポートでエミュレータを起動しているので、ホストを 58080 ポートで指定しています。
「データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テスト
const db = supporter.getFirestoreWithAuth("other_user"); の部分で、商品管理者 admin_user とは異なるユーザ other_user で認証された Firestore クライアントを取得しています。
ルールの実装
商品データ追加用のテストが完成したので、テスト駆動でセキュリティルールの実装を進めていきます。
テスト駆動のスタイルに従い、まずはテストを実施してみて、テストが失敗することを確認します。
テストをスタート
以下のコマンドを実行し、テストをスタートしてください。
1npm run test-watch tests/data.add.test.ts
テスト結果
各テストの結果に、以下のようなエラーメッセージが表示されて、テストが失敗します。
1ENOENT: no such file or directory, open '.../tests/firestore.rules'
全アクセスを拒否するセキュリティルールを設定
tests/firestore.rules ファイルがないという事なので、tests/firestore.rules ファイルを追加して、以下のように記述してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 // 全てのアクセスを拒否
5 allow read, wreite: if false;
6 }
7}
まずは、全てのアクセスを拒否するとこらからスタートし、上から順にテストを通して行きます。
テスト結果
テストが自動で再実施され、テスト結果の冒頭に以下のような結果が表示されます。
1FAIL tests/data.add.test.ts
2 データ追加テスト
3 × 要件にあったデータの追加に成功 (2291ms)
4 √ データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (53ms)
テスト駆動開発では、失敗するテストを順次追加していくのが一般的です。
しかし、この記事では、解説のわかりやすさのため、テストをまとめて追加しています。
そのため、一部テストが通ってしまっていますが、とりあえずスルーしてください。
コレクションへの全アクセスを許可するセキュリティルールを設定
「要件にあったデータの追加に成功」テストが失敗しているので、とりあえず items コレクションへのアクセスを全て許可してみます。
以下のようにコードを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow read, write: if true;
6 }
7 }
8}
テスト結果
テスト結果は、以下の通りになります。
1 FAIL tests/data.add.test.ts
2 データ追加テスト
3 √ 要件にあったデータの追加に成功 (2095ms)
4 × データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (58ms)
「要件にあったデータの追加に成功」テストが成功しました。
次に、「データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可」テストを通します。
ユーザ情報を許可条件の設定に使用する
以下のようにルールを調整してください。
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4 match /items/{item_id}{
5 allow create: if request.auth.uid == request.resource.data.admin_user;
6 }
7 }
8}
ここで、request.auth フィールドにより、リクエストしたユーザの情報を、許可条件の設定に使用することができます。
uid のほか、email や phone_number などを参照できます。
また、request.resource.data フィールドにより、追加データの情報を条件に使用することができます。
なお、request.auth のより詳しい内容は、以下を参照してください。
【参考】
Interface: Request | Firebase - auth
テスト結果
テスト結果は、以下の通りとなります。
1 PASS tests/data.add.test.ts
2 データ追加テスト
3 √ 要件にあったデータの追加に成功 (2095ms)
4 √ データ追加をリクエストしたユーザが商品管理者と一致しない場合は追加不可 (56ms)
テストが全て通りましたので、商品データ追加向けのセキュリティルールの実装はこれで完了です。
次のテストとの競合を避けるために、データ追加用のテストを終了してください。
後編へつづく!
以上、「実装目標の設定」から、「データ追加ルールを実装する」ところまで解説してみました。
後編では、データ更新と取得のルールを、今回と同じように、テスト駆動で実装していきます。
また、データベースにすでにあるデータの利用などについても解説していきます。
それでは、続きは後編で!
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.08.04エンジニアの働き方 特集社員としての働き方社員としてのエンジニアの働き方とは?ライトコードのエンジニアはどんな働き方をしてるのか、まとめたいと...
2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...
後編の記事はこちら!
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit
おすすめ記事
immichを知ってほしい
2024.10.31