• トップ
  • ブログ一覧
  • テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第1回】
  • テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第1回】

    広告メディア事業部広告メディア事業部
    2020.06.04

    IT技術

    テスト駆動で成績データコレクションのルールを実装する【第1回】

    この記事では、全4回に分けて、Firestore セキュリティルールでのカスタム関数の利用について解説していきます。

    成績評価システムを想定した、成績データコレクション records のルール実装を例に、解説を進めていきます。

    なお、今回の「カスタム関数編」では、これまでの「テスト駆動で学ぶ Firestore セキュリティルール」シリーズの内容を前提としていますので、以下の記事をご参照ください。

    データ検証編はこちら

    「データ検証編」では、リクエストデータ、データベース上のデータの参照などについて解説

    featureImg2020.04.22テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】テスト駆動で商品データのルールを実装するこの記事では、セキュリティルールによるデータ検証について、商品データのルールを...

    データ比較編はこちら

    「データ比較編」では、データの比較や型チェックなどについて解説

    featureImg2020.05.07テスト駆動で学ぶ Firestore セキュリティルール 【データ比較編 / 前編】テスト駆動で書籍コレクションのルールを実装するこの記事では、前編・後編の二回に分けて、Firestore セキュリティ...

    カスタム関数編の簡単な流れ

    「カスタム関数編」では、以下のような流れで解説を進めていきます。

    1. 第1回 : カスタム関数の簡単な解説と実装ルールの要件設定&前準備
    2. 第2回 : データ追加用のルールの作成
    3. 第3回 : データ更新用のルールの作成
    4. 第4回 : データ取得・削除用のルールの作成

    今回の内容は?

    第1回である今回は、最初に「データ比較編」で作成したセキュリティルールをカスタム関数を使って整理しながら、カスタム関数について簡単に解説します。

    その後、この記事から始まる「カスタム関数編」の全4回を通して、ルールを実装する成績データコレクション records の要件を設定します。

    テストで必要となるコレクション usersseasons の要件も併せて設定します。

    続いて、ルール実装の前準備として、テスト環境とテストデータを準備します。

    カスタム関数について

    まずは、前回の「データ比較編」で実装したルールを例に、カスタム関数について簡単に解説したいと思います。

    データ比較編」では、書店のネットショッピングサービスを例に、書籍コレクション books のルールを実装しました。

    そのとき実装したルールは、以下の通りです。

    「データ比較編」で実装したルール

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      allow create: if request.auth.uid in request.resource.data.adminUsers
    6                        && request.resource.data.size() == 9
    7                        && request.resource.data.title is string
    8                        && request.resource.data.description is string
    9                        && request.resource.data.releaseDate is timestamp
    10                        && request.resource.data.price is int
    11                        && request.resource.data.price >= 0
    12                        && request.resource.data.stock is int
    13                        && request.resource.data.stock >= 0
    14                        && request.resource.data.condition in ["new","used"];
    15 
    16      allow update: if request.auth.uid in resource.data.adminUsers
    17                        && request.resource.data.size() == 9
    18                        && request.resource.data.title is string
    19                        && request.resource.data.description is string
    20                        && request.resource.data.releaseDate is timestamp
    21                        && request.resource.data.price is int
    22                        && request.resource.data.price >= 0
    23                        && request.resource.data.stock is int
    24                        && request.resource.data.stock >= 0
    25                        && request.resource.data.condition in ["new","used"];
    26 
    27     allow get: if resource.data.draft != true
    28                  && resource.data.stock > 5;
    29    }
    30  }
    31}

    get アクセスについては、「下書きデータでなく、かつ在庫が6以上」であれば取得が可能となっています。

    createupdate アクセスは、大雑把に、どちらも書籍の管理担当者チェックと、書籍データのフォーマットチェックを実行。

    書籍の管理担当者であり、かつリクエストデータが適切なフォーマットであれば、データの追加・更新が可能です。

    ルール全体の見通しが悪くなっているため調整する

    フォーマットチェックの内容が多く、これにより「allow式」が長くなってしまっているため、ルール全体の見通しが悪くなっています。

    今回の記事のテーマとなるカスタム関数を使って、上記のルールを整理し、見通しの良いルールに調整したいと思います。

    上述のルールは、カスタム関数を使って、以下のように書き換えることが可能です。

    「データ比較編」で実装したルールをカスタム関数で書き替える

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /books/{id}{
    5      // 書籍の管理担当者チェック関数
    6      function isAdminUser(_resource){        
    7          return request.auth.uid in _resource.data.adminUsers;
    8      }
    9
    10      // 書籍データのフォーマットチェック関数
    11      function validBookData(){
    12        return request.resource.data.size() == 9
    13                        && request.resource.data.title is string
    14                        && request.resource.data.description is string
    15                        && request.resource.data.releaseDate is timestamp
    16                        && request.resource.data.price is int
    17                        && request.resource.data.price >= 0
    18                        && request.resource.data.stock is int
    19                        && request.resource.data.stock >= 0
    20                        && request.resource.data.condition in ["new","used"];
    21      }
    22
    23      allow create: if isAdminUser(request.resource)
    24                        && validBookData();
    25
    26 
    27      allow update: if isAdminUser(resource)
    28                        && validBookData();
    29 
    30     allow get: if resource.data.draft != true
    31                  && resource.data.stock > 5;
    32    }
    33  }
    34}

    ここで、カスタム関数 isAdminUser() を定義して、書籍の管理担当者チェック部分を関数化しています。

    create アクセスでは、リクエストデータの、update アクセスでは、データベース上のデータの書籍管理担当者をチェックするため、参照するデータが異なります。

    リクエストデータ、データベース上のデータ、どちらのデータのチェックにも対応できるように、チェック対象のデータリソースを isAdminUser() 関数の引数で指定できるように定義。

    また、validBookData() 関数を定義して、リクエストデータのフォーマットチェックを1つにまとめています。

    カスタム関数でのルール整理で見通しがよくなった

    カスタム関数によるルールの整理によって、「allow式」が短くなって、ルールの見通しがよくなりました。

    ここでの解説と同様、「カスタム関数編」では、成績評価システムを想定した成績コレクション records のセキュリティルールを例に、カスタム関数を使ってルールを整理しながらセキュリティルールの実装を進めていきます。

    成績データコレクションの要件(実装目標)を設定

    まずは、成績コレクション records と、テストで必要となる users コレクションseasons コレクションの要件を設定します。

    成績コレクション records の要件は、以下の通りに設定します。

    コレクションパス

    students/{studentId}/records

    ロール

    adminシステム管理者
    teacher教師
    student生徒

    record ドキュメントのフォーマット

    データ名称内容データ型条件
    id成績IDstring型必須。変更不可。
    studentId生徒IDstring型必須。変更不可。
    season年度と学期map型必須。変更不可。
    name生徒の名前string型必須
    homeroomTeacher担任string型必須
    record成績map型なし

    各処理共通の要件

    1. ログインしていないユーザはアクセス不可

    データ追加要件

    1. admin ユーザ以外は追加不可
    2. 成績フィールドは空のマップ
    3. 所定のフォーマットでないデータは追加不可

    成績評価の設定は、teacher ユーザがデータ更新でのみ設定可能とします。

    データ追加時に成績が設定されないように、追加時の成績フィールドは、空のマップのみ設定可能としています。

    データ更新要件

    admin ユーザの更新要件

    1. id の更新不可
    2. studentId の更新不可
    3. season の更新不可
    4. record の更新不可

    teacher ユーザの更新要件

    1. 担任でないデータは更新不可
    2. 成績以外のフィールドは更新不可
    3. 対象シーズンが成績評価期間外の場合は更新不可

    その他の更新要件

    1. student ユーザは更新不可
    2. 所定のフォーマットでないデータは更新不可

    データ取得要件

    1. admin ユーザはデータの取得可

    teacher ユーザの取得要件

    1. 担任でないデータは取得不可

    student ユーザの取得要件

    1. 自分以外のデータは取得不可
    2. 対象シーズンが成績評価期間の場合は取得不可

    データ削除要件

    1. admin ユーザ以外は削除不可

    関連コレクションの要件を設定

    以下、テストの中で records コレクション以外に必要となるコレクションの要件を簡単に設定します。

    「seasons」コレクションのフォーマット

    データ名称内容データ型条件
    idシーズンID。{year}_{semester} のようなフォーマットで設定。string型必須。変更不可。
    evaluationPeriod成績評価期間bool型必須

    成績評価期間の更新は、admin ユーザだけが行えるものと想定します。

    前述の records コレクションの要件設定でふれた通り、評価期間中のみ teacher ユーザが担当生徒の成績評価を更新できるものとします。

    「users」コレクションのフォーマット

    データ名称内容データ型条件
    idユーザIDstring型必須。変更不可。
    rolesロール。adminteacherまたはstudentを設定。list型必須

    roles は、複数ロールを設定する場合を想定して「list型」としています。

    例えば、admin と teacher ロールを兼任する場合や、他のロールを追加する場合などを想定しています。

    seasonsusers コレクションは、要件の設定のみで、本記事では、ルールの実装・解説はしません。

    前準備 ~ テスト環境 ~

    以下のリポジトリに、本記事のコードをまとめてあります。

    【GitHub】
    rightcode/firestore-security-rules-test_custom-function

    テスト環境のセットアップ

    以下のコマンドを実行して、テスト環境をセットアップしてください。

    1git clone "https://github.com/rightcode/firestore-security-rules-test_custom-function" sandbox
    2cd sandbox
    3git checkout refs/tags/test-environment

    Firebase CLI ツールのインストール

    Firebase CLI ツールをインストールしていない場合は、以下のコマンドを実行してインストールしてください。

    1npm install -g firebase-tools

    npm パッケージのインストール

    以下のコマンドを実行して、必要となる npm パッケージをインストールしてください。

    1npm install

    Firestore エミュレータを起動

    以下のコマンドを実行して、Firestore エミュレータを起動してください。

    1firebase setup:emulators:firestore
    2firebase emulators:start --only firestore

    以上で、テスト環境の準備は完了です。

    テスト環境のセットアップの解説

    テスト環境のセットアップの、より詳細な内容は、以下の記事をご参照ください。

    featureImg2020.02.12Firestoreエミュレータ+Jestでセキュリティルールをテストする!Firestore(ファイアストア)とは?「Firestore」は、Google製のスケーラブルなドキュメントデータベ...

    master ブランチに実装済みのコードを設置してありますので、本記事中のコードを確認する場合は、以下のコマンドよりmaster ブランチをチェックアウトしてください。

    1git checkout master

    コードの調整などにより、本記事の内容とは記述が若干異なる場合があります。

    前準備 ~ テストデータ ~

    テストを作成する前に、まず、テストデータ用のコードを用意します。

    以下の4つファイルを作成してください。

    1. tests/data/collections/records.ts
    2. tests/data/collections/seasons.ts
    3. tests/data/collections/users.ts
    4. tests/data/InitialData.ts

    追加したファイルに、コードを追加していきます。

    成績データコレクションのテストデータ

    tests/data/collections/records.ts に、以下のコードを追加してください。

    1import { users } from "./users";
    2import { seasons } from "./seasons";
    3
    4export namespace records {
    5    export namespace sample {
    6        // 成績ID
    7        export const id = "YYYYYYYYYY";
    8
    9        // 成績
    10        export const record = {
    11            Math: 4,
    12            NationalLanguage: 3
    13        };
    14    }
    15
    16    // コレクションパス
    17    export const collectionPath = "students/" + users.sample.student + "/records";
    18
    19    // 初期データ
    20    export const initialData = {
    21        id: sample.id,
    22        studentId: users.sample.student,
    23        season: seasons.sample.season,
    24        name: users.sample.student,
    25        homeroomTeacher: users.sample.teacher,
    26        record: {}
    27    };
    28
    29    // 更新用データ/システム管理者向け
    30    export const validUpdateDataForAdmin = {
    31        id: sample.id,
    32        studentId: users.sample.student,
    33        season: seasons.sample.season,
    34        name: "Eto",
    35        homeroomTeacher: "Fujita0000",
    36        record: {}
    37    };
    38
    39    // 更新用データ/教師向け
    40    export const validUpdateDataForTeacher = {
    41        id: sample.id,
    42        studentId: users.sample.student,
    43        season: seasons.sample.season,
    44        name: users.sample.student,
    45        homeroomTeacher: users.sample.teacher,
    46        record: {
    47            Math: 5,
    48            NationalLanguage: 5
    49        }
    50    };
    51}

    「seasons」コレクションのテストデータ

    tests/data/collections/seasons.ts に、以下のコードを追加してください。

    1export namespace seasons {
    2    export namespace sample {
    3        // 対象シーズン
    4        export const season = "2010_2"
    5    }
    6
    7    // コレクションパス
    8    export const collectionPath = "seasons";
    9
    10    // 初期データ
    11    export const initialData = (evaluationPeriod: boolean) => ({
    12        id: sample.season,
    13        evaluationPeriod: evaluationPeriod
    14    });
    15}

    「users」コレクションのテストデータ

    tests/data/collections/users.ts に、以下のコードを追加してください。

    1export namespace users {
    2    export namespace sample {
    3        // システム管理者
    4        export const admin = "Aida0000";
    5
    6        // 教師
    7        export const teacher = "Baba0000";
    8
    9        // 生徒
    10        export const student = "Dojima0000";
    11
    12        // その他の生徒
    13        export const other_student = "Fujita"
    14    }
    15
    16    // コレクションパス
    17    export const collectionPath = "users";
    18
    19    // 初期データ
    20    const _initialData: any = {};
    21    _initialData[sample.admin] = {
    22        id: sample.admin,
    23        roles: ["admin"]
    24    };
    25    _initialData[sample.teacher] = {
    26        id: sample.teacher,
    27        roles: ["teacher"]
    28    };
    29    _initialData[sample.student] = {
    30        id: sample.student,
    31        roles: ["student"]
    32    };
    33    _initialData[sample.other_student] = {
    34        id: sample.other_student,
    35        roles: ["student"]
    36    };
    37    export const initialData = _initialData;
    38}

    初期データのテストデータ

    tests/data/InitialData.ts に、以下のコードを追加してください。

    1import { FirestoreTestSupporter } from "firestore-test-supporter";
    2
    3import * as firebase from "@firebase/testing";
    4import { users } from "./collections/users";
    5import { records } from "./collections/records";
    6import { seasons } from "./collections/seasons";
    7
    8export default class InitialData {
    9    private supporter: FirestoreTestSupporter;
    10    private db: firebase.firestore.Firestore;
    11
    12    constructor(rulesFilePath: string) {
    13        this.supporter = new FirestoreTestSupporter("my-test-project", rulesFilePath);
    14        this.db = this.supporter.getAdminFirestore();
    15    }
    16
    17    // usersコレクションに初期データを追加
    18    async setupUsers() {
    19        for (const userId of Object.keys(users.initialData)) {
    20            const userDoc = this.db.collection(users.collectionPath).doc(userId);
    21            await firebase.assertSucceeds(userDoc.set(users.initialData[userId]))
    22        }
    23    }
    24
    25    // recordsコレクションに初期データを追加
    26    async setupRecords() {
    27        const recordDoc = this.db.collection(records.collectionPath).doc(records.initialData.id);
    28        await firebase.assertSucceeds(recordDoc.set(records.initialData))
    29    }
    30
    31    // seasonsコレクションに初期データを追加
    32    async setupSeasons() {
    33        const seasonDoc = this.db.collection(seasons.collectionPath).doc(seasons.initialData(true).id);
    34        await firebase.assertSucceeds(seasonDoc.set(seasons.initialData(true)))
    35    }
    36}

    テストデータの概要

    tests/data /collections/records.ts

    records コレクションのサンプルデータを定義しています。

    初期データ、更新用データには records.tsseasons.tsusers.ts で定義しているテスト用のサンプルデータを使用しています。

    tests/data/collections/seasons.ts

    seasons コレクションのサンプルデータを定義しています。

    初期データには、引数により成績評価期間フラグの設定が可能です。

    tests/data/collections/users.ts

    users コレクションのサンプルデータを定義しています。

    admin, teacher, student ロールのサンプルユーザを定義しています。

    tests/data/InitialData.ts

    初期データのセットアップ用クラス。

    各テストの前に必要な初期データをデータベースに追加します。

    第2回へつづく!

    「カスタム関数編」第1回となるこの記事では、前回の記事「データ比較編」で作成したセキュリティルールを例に、カスタム関数の使い方を簡単に解説してみました。

    また、カスタム関数の利用例として扱う、成績データコレクション records の要件を設定し、ルール実装に使用するテスト環境とテストデータを準備しました。

    次回は、今回設定した要件に沿って、データ追加用ルールの実装を進めていきます。

    お楽しみに!

    第2回の記事はこちら

    featureImg2020.04.22テスト駆動で学ぶ Firestore セキュリティルール 【データ検証編 / 前編】テスト駆動で商品データのルールを実装するこの記事では、セキュリティルールによるデータ検証について、商品データのルールを...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.08.04エンジニアの働き方 特集社員としての働き方社員としてのエンジニアの働き方とは?ライトコードのエンジニアはどんな働き方をしてるのか、まとめたいと...

    featureImg2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background