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

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

    IT技術

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

    前回の記事では、成績コレクション records のデータ追加ルールを実装すると共に、カスタム関数を利用したルールの整理について解説してみました。

    今回は、成績コレクション records のデータ更新ルールの実装を進めると共に、引き続き、カスタム関数を利用したルールの整理について解説していきます。

    前回の記事はこちら

    前回の記事は、こちらをご参照ください。

    featureImg2020.06.05テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第2回】【第2回】テスト駆動で成績データコレクションのルールを実装する前回は、カスタム関数について簡単に解説した後、「カスタム...

    Firestore 上のデータ更新ルールの実装について

    テストの作成前に、まずは、データ更新の要件を再確認します。

    admin ユーザの更新要件

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

    teacher ユーザの更新要件

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

    その他の更新要件

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

    上記の要件に沿って、テストを作成していきます。

    tests/data.update.test.ts ファイルを追加して、以下のようにコードを記述してください。

    テストのコード

    1process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080";
    2
    3import { FirestoreTestSupporter } from "firestore-test-supporter";
    4import * as path from "path";
    5import * as firebase from "@firebase/testing";
    6import { users } from "./data/collections/users";
    7import { records } from "./data/collections/records";
    8import { seasons } from "./data/collections/seasons";
    9import InitialData from "./data/InitialData";
    10
    11describe("成績データの更新テスト", () => {
    12    const rulesFilePath = path.join(__dirname, "firestore.rules");
    13    const supporter = new FirestoreTestSupporter("my-test-project", rulesFilePath);
    14
    15    beforeEach(async () => {
    16        await supporter.loadRules();
    17
    18        // 各コレクションに初期データを追加
    19        const initialData = new InitialData(rulesFilePath);
    20        await initialData.setupUsers();
    21        await initialData.setupRecords();
    22        await initialData.setupSeasons();
    23    });
    24
    25    afterEach(async () => {
    26        await supporter.cleanup()
    27    });
    28
    29    test('ログインしていないユーザは更新不可', async () => {
    30        // 認証されていないクライアントを取得
    31        const db = supporter.getFirestore();
    32
    33        const doc = db.collection(records.collectionPath).doc(records.initialData.id);
    34        await firebase.assertFails(doc.update(records.initialData))
    35    });
    36
    37    describe("adminユーザの更新要件", () => {
    38        let doc: firebase.firestore.DocumentReference;
    39        beforeEach(() => {
    40            // adminユーザで認証されたクライアントを取得
    41            const db = supporter.getFirestoreWithAuth(users.sample.admin);
    42
    43            doc = db.collection(records.collectionPath).doc(records.initialData.id);
    44        });
    45
    46        test('要件にあったデータの更新に成功', async () => {
    47            await firebase.assertSucceeds(doc.update(records.validUpdateDataForAdmin));
    48        });
    49
    50        test('idの更新不可', async () => {
    51            // リクエストデータのidを変更
    52            const badData = { ...records.validUpdateDataForAdmin, id: "other_id" };
    53
    54            await firebase.assertFails(doc.update(badData));
    55        });
    56
    57        test('studentIdの更新不可', async () => {
    58            // リクエストデータのstudentIdを変更
    59            const badData = { ...records.validUpdateDataForAdmin, studentId: "other0000" };
    60
    61            await firebase.assertFails(doc.update(badData));
    62        });
    63
    64        test('seasonの更新不可', async () => {
    65            // リクエストデータのseasonを変更
    66            const badData = { ...records.validUpdateDataForAdmin, season: { year: 2011, semester: 3 } };
    67
    68            await firebase.assertFails(doc.update(badData));
    69        });
    70
    71        test('recordの更新不可', async () => {
    72            // リクエストデータのrecordを変更
    73            const badData = { ...records.validUpdateDataForAdmin, record: { Math: 3, NationalLanguage: 4 } };
    74
    75            await firebase.assertFails(doc.update(badData));
    76        });
    77    });
    78
    79    describe("teacherユーザの更新要件", () => {
    80        let doc: firebase.firestore.DocumentReference;
    81        beforeEach(() => {
    82            // teacherユーザで認証されたクライアントを取得
    83            const db = supporter.getFirestoreWithAuth(users.sample.teacher);
    84
    85            doc = db.collection(records.collectionPath).doc(records.initialData.id);
    86        });
    87
    88        test('要件にあったデータの更新に成功', async () => {
    89            await firebase.assertSucceeds(doc.update(records.validUpdateDataForTeacher));
    90        });
    91
    92        test('担任でないデータは更新不可', async () => {
    93            // データベース上のデータの担任を変更
    94            const dbWithAdmin = supporter.getAdminFirestore();
    95            const docWithAdmin = dbWithAdmin.collection(records.collectionPath).doc(records.initialData.id);
    96            const newData = { ...records.validUpdateDataForTeacher, homeroomTeacher: "other_teacher" };
    97            await firebase.assertSucceeds(docWithAdmin.update(newData));
    98
    99            await firebase.assertFails(doc.update(records.validUpdateDataForTeacher));
    100        });
    101
    102        describe('成績以外のフィールドは更新不可', () => {
    103            test('idの更新不可', async () => {
    104                // リクエストデータのidを変更
    105                const badData = { ...records.validUpdateDataForTeacher, id: "other_id" };
    106
    107                await firebase.assertFails(doc.update(badData));
    108            });
    109
    110            test('studentIdの更新不可', async () => {
    111                // リクエストデータのstudentIdを変更
    112                const badData = { ...records.validUpdateDataForTeacher, studentId: "other0000" };
    113
    114                await firebase.assertFails(doc.update(badData));
    115            });
    116
    117            test('seasonの更新不可', async () => {
    118                // リクエストデータのseasonを変更
    119                const badData = { ...records.validUpdateDataForTeacher, season: { year: 2009, semester: 1 } };
    120
    121                await firebase.assertFails(doc.update(badData));
    122            });
    123
    124            test('nameの更新不可', async () => {
    125                // リクエストデータのnameを変更
    126                const badData = { ...records.validUpdateDataForTeacher, name: "other_name" };
    127
    128                await firebase.assertFails(doc.update(badData));
    129            });
    130
    131            test('homeroomTeacherの更新不可', async () => {
    132                // リクエストデータのhomeroomTeacherを変更
    133                const badData = { ...records.validUpdateDataForTeacher, homeroomTeacher: "other_id" };
    134
    135                await firebase.assertFails(doc.update(badData));
    136            });
    137        });
    138
    139        test('対象シーズンが成績評価期間外の場合は更新不可', async () => {
    140            // データベース上のデータの成績評価期間フラグをfalseに変更
    141            const dbWithAdmin = supporter.getAdminFirestore();
    142            const seasonDoc = dbWithAdmin.collection(seasons.collectionPath).doc(seasons.initialData(true).id);
    143            await firebase.assertSucceeds(seasonDoc.update(seasons.initialData(false)));
    144
    145            await firebase.assertFails(doc.update(records.validUpdateDataForTeacher));
    146        });
    147    });
    148
    149    test('studentユーザは更新不可', async () => {
    150        // studentユーザで認証されたクライアントを取得
    151        const db = supporter.getFirestoreWithAuth(users.sample.student);
    152
    153        const doc = db.collection(records.collectionPath).doc(records.initialData.id);
    154        await firebase.assertFails(doc.update(records.initialData));
    155    });
    156});

    テストの概要

    「admin ユーザの更新要件」テスト

    beforeEach() 関数内で、各テスト前に admin ユーザで認証されたクライアントを取得しています。

    「teacher ユーザの更新要件」テスト

    adminユーザの更新要件 テストと同様に、teacher ユーザで認証されたクライアントを取得しています。

    担任でないデータは更新不可 となるテストでは、データ更新前に、データベース上のデータの担任を、リクエストデータの担任とは異なる担任に変更

    対象シーズンが成績評価期間外の場合は更新不可 となるテストでは、データ更新の前に、seasons コレクションの対象となる、 season ドキュメントの成績評価期間フラグを「false」に変更しています。

    「所定のフォーマットでないデータは更新不可」となる要件について

    データ更新については、フォーマットチェックテストが複雑になるため、解説の便宜上、テストを作成していません。

    リクエストデータのフォーマットは、データ追加の場合と同じフォーマットを要件としています。

    フォーマットチェックには、前回の記事で定義した、フォーマットチェック用関数 isValidFormat() を使用。

    他のテストの結果に、影響がないかどうかだけを確認します。

    Firestore 上のデータ更新ルールを実装してみる

    それでは、作成したテストに沿って、セキュリティルールを実装していきましょう。

    セキュリティルールが少し長くなってきているので、ここでは、前回の記事で実装したルールとは分けて、データ更新のルールだけを実装していきます

    そして、最後に、データ追加用のルールとデータ更新用のルールを統合したいと思います。

    全ての update アクセスを許可するルールを設定

    データ追加ルールの実装と同様、まずは、records コレクションの update アクセスを全て許可し、「要件にあったデータの追加に成功」するテストを通します

    その後、失敗したテストを順次、上から通していきます。

    セキュリティルールの調整

    以下のように、セキュリティルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      // 全てのupdateアクセスを許可
    6      allow update: if true;
    7    }
    8  }
    9}

    テストをスタート

    ルールを調整したら、以下のコマンドを実行して、データ更新ルール用のテストをスタートしてください。

    1npm run test-watch tests/data.update.test.ts

    テスト結果

    1 FAIL  tests/data.update.test.ts
    2  成績データの更新テスト
    3    × ログインしていないユーザは更新不可 (1171ms)
    4    × studentユーザは更新不可 (64ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (87ms)
    7      × idの更新不可 (87ms)
    8      × studentIdの更新不可 (68ms)
    9      × seasonの更新不可 (71ms)
    10      × recordの更新不可 (66ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (63ms)
    13      × 担任でないデータは更新不可 (1152ms)
    14      × 対象シーズンが成績評価期間外の場合は更新不可 (99ms)
    15      成績以外のフィールドは更新不可
    16        × idの更新不可 (70ms)
    17        × studentIdの更新不可 (64ms)
    18        × seasonの更新不可 (65ms)
    19        × nameの更新不可 (71ms)
    20        × homeroomTeacherの更新不可 (67ms)

    admin ユーザと teacher ユーザに対して、要件にあったデータの更新に成功 するテストが成功しました。

    続いて、失敗しているテストを上から順に通していきたいと思います。

    「admin, teacher ユーザ以外は更新不可」となるルールを設定

    ログインしていないユーザは更新不可studentユーザは更新不可 となるように、admin ユーザか teacher ユーザでない場合は、更新不可となるルールを追加します。

    データ追加ルールの実装で定義した isAdmin() 関数と同様、新しく isTeacher() 関数を定義します。

    以下のようにルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      // teacherロールチェック
    19      function isTeacher(){
    20        return isUserRole("teacher");
    21      }
    22
    23      allow update: if isAdmin();
    24                        
    25      allow update: if isTeacher();
    26    }
    27  }
    28}

    ここで、isTeacher() 関数では、データ追加ルールの実装の最後で定義したロールチェック用関数 isUserRole() を使用し、リクエストユーザが teacher ロールを持っているかどうかをチェックしています。

    isAdmin() 関数と同様、isTeacher() 関数にはログインチェックも含んでいます。

    また、admin ユーザと teacher ユーザで要件が異なるため、以降のルール調整に備えて、ルールを admin ユーザと teacher ユーザで分けて記述しました。

    テスト結果

    1 FAIL  tests/data.update.test.ts
    2  成績データの更新テスト
    3ログインしていないユーザは更新不可 (154ms)
    4studentユーザは更新不可 (72ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (109ms)
    7      × idの更新不可 (121ms)
    8      × studentIdの更新不可 (92ms)
    9      × seasonの更新不可 (81ms)
    10      × recordの更新不可 (87ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (1247ms)
    13      × 担任でないデータは更新不可 (130ms)
    14      × 対象シーズンが成績評価期間外の場合は更新不可 (91ms)
    15      成績以外のフィールドは更新不可
    16        × idの更新不可 (69ms)
    17        × studentIdの更新不可 (72ms)
    18        × seasonの更新不可 (83ms)
    19        × nameの更新不可 (77ms)
    20        × homeroomTeacherの更新不可 (84ms)

    ログインしていないユーザは更新不可studentユーザは更新不可 となるテストが通りました。

    「admin ユーザの更新要件」をルールに設定

    続いて、adminユーザの更新要件 の更新不可テストを、まとめて通してしまいます。

    以下のように、ルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      function isTeacher(){
    19        return isUserRole("teacher");
    20      }
    21
    22      // 指定フィールドの更新なしチェック
    23      function isNotUpdate(key){
    24        return request.resource.data[key] == resource.data[key]
    25      }
    26
    27      allow update: if isAdmin()
    28                        && isNotUpdate("id")
    29                        && isNotUpdate("studentId")
    30                        && isNotUpdate("season")
    31                        && isNotUpdate("record");
    32                        
    33      allow update: if isTeacher();
    34    }
    35  }
    36}

    request.resource.data.some_field == resource.data.some_field のように記述することで、対象フィールドを変更不可とすることができます。

    どのフィールドでも同様の記述となるので、重複を避けるために isNotUpdate() 関数を定義しています

    テスト結果

    1 FAIL  tests/data.update.test.ts
    2  成績データの更新テスト
    3ログインしていないユーザは更新不可 (1160ms)
    4studentユーザは更新不可 (69ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (82ms)
    7idの更新不可 (80ms)
    8studentIdの更新不可 (67ms)
    9seasonの更新不可 (73ms)
    10recordの更新不可 (68ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (63ms)
    13      × 担任でないデータは更新不可 (94ms)
    14      × 対象シーズンが成績評価期間外の場合は更新不可 (96ms)
    15      成績以外のフィールドは更新不可
    16        × idの更新不可 (65ms)
    17        × studentIdの更新不可 (68ms)
    18        × seasonの更新不可 (60ms)
    19        × nameの更新不可 (1122ms)
    20        × homeroomTeacherの更新不可 (69ms)

    adminユーザの更新要件 テストが全て通ったので、これで admin ユーザに対するルール設定は完了です。

    「teacher ユーザの更新要件」をルールに設定

    続いて、teacherユーザの更新要件 テストを通して行きます。

    担任でないデータは更新不可対象シーズンが成績評価期間外の場合は更新不可 となるよう、以下のようにルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      function isTeacher(){
    19        return isUserRole("teacher");
    20      }
    21
    22      function isNotUpdate(key){
    23        return request.resource.data[key] == resource.data[key]
    24      }
    25
    26      // 担任チェック
    27      function isHomeroomTeacher(){
    28        return request.auth.uid == resource.data.homeroomTeacher
    29      }
    30
    31      // 成績評価期間チェック
    32      function isEvaluationPeriod(){
    33        return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod
    34      }
    35      
    36      allow update: if isAdmin()
    37                        && isNotUpdate("id")
    38                        && isNotUpdate("studentId")
    39                        && isNotUpdate("season")
    40                        && isNotUpdate("record");
    41                        
    42      allow update: if isTeacher()
    43                        && isHomeroomTeacher()
    44                        && isEvaluationPeriod();
    45    }
    46  }
    47}

    ここで、isHomeroomTeacher() 関数により、リクエストユーザが対象となる成績データの担任であるかどうかを判定しています。

    isEvaluationPeriod() 関数は、seasons コレクションの対象となる season ドキュメントを参照して、成績評価期間フラグをチェックしています。

    テスト結果

    1 FAIL  tests/data.update.test.ts (5.181s)
    2  成績データの更新テスト
    3ログインしていないユーザは更新不可 (2122ms)
    4studentユーザは更新不可 (64ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (94ms)
    7idの更新不可 (76ms)
    8studentIdの更新不可 (75ms)
    9seasonの更新不可 (71ms)
    10recordの更新不可 (62ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (68ms)
    13担任でないデータは更新不可 (91ms)
    14対象シーズンが成績評価期間外の場合は更新不可 (86ms)
    15      成績以外のフィールドは更新不可
    16        × idの更新不可 (1108ms)
    17        × studentIdの更新不可 (72ms)
    18        × seasonの更新不可 (69ms)
    19        × nameの更新不可 (63ms)
    20        × homeroomTeacherの更新不可 (68ms)

    担任でないデータは更新不可 となるテストと、対象シーズンが成績評価期間外の場合は更新不可 となるテストが通りました。

    「成績以外のフィールドは更新不可」となるルールを設定

    最後に、成績以外のフィールドは更新不可 となるテストをまとめて通してしまいます。

    先ほど定義した isNotUpdate() 関数を使って、以下のようにルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      function isTeacher(){
    19        return isUserRole("teacher");
    20      }
    21
    22      function isNotUpdate(key){
    23        return request.resource.data[key] == resource.data[key]
    24      }
    25
    26      function isHomeroomTeacher(){
    27        return request.auth.uid == resource.data.homeroomTeacher
    28      }
    29
    30      function isEvaluationPeriod(){
    31        return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod
    32      }
    33
    34      allow update: if isAdmin()
    35                        && isNotUpdate("id")
    36                        && isNotUpdate("studentId")
    37                        && isNotUpdate("season")
    38                        && isNotUpdate("record");
    39                        
    40      allow update: if isTeacher()
    41                        && isHomeroomTeacher()
    42                        && isEvaluationPeriod()
    43                        && isNotUpdate("id")
    44                        && isNotUpdate("studentId")
    45                        && isNotUpdate("season")
    46                        && isNotUpdate("name")
    47                        && isNotUpdate("homeroomTeacher");
    48    }
    49  }
    50}

    テスト結果

    1 PASS  tests/data.update.test.ts
    2  成績データの更新テスト
    3ログインしていないユーザは更新不可 (100ms)
    4studentユーザは更新不可 (91ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (89ms)
    7idの更新不可 (81ms)
    8studentIdの更新不可 (84ms)
    9seasonの更新不可 (70ms)
    10recordの更新不可 (72ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (70ms)
    13担任でないデータは更新不可 (88ms)
    14対象シーズンが成績評価期間外の場合は更新不可 (90ms)
    15      成績以外のフィールドは更新不可
    16idの更新不可 (62ms)
    17studentIdの更新不可 (63ms)
    18seasonの更新不可 (63ms)
    19nameの更新不可 (75ms)
    20homeroomTeacherの更新不可 (70ms)

    全てのテストが通ったので、これで要件に沿ったデータ更新用のルールを実装することができました

    カスタム関数でルールを整理

    データ追加ルールの実装のときと同じように、今回もテスト環境を活かしてルールをもう少しだけ調整・整理したいと思います。

    isNotUpdateImmutables() 関数を定義

    record ドキュメントのフォーマット定義で変更不可としたフィールドは、当然ながら、admin ユーザと teacher ユーザの両方が更新不可となります。

    このため、「allow式」中で、変更不可チェック用に追加した条件が重複しています。

    これらのフィールドの不変チェックをひとまとめにして、isNotUpdateImmutables() 関数を定義します。

    リクエストデータのフォーマットチェック

    また、リクエストデータのフォーマットは、データ追加の場合と同じフォーマットです。

    そのため、データ追加ルールの実装時に定義した isValidFormat() 関数を使い、リクエストデータのフォーマットチェックを行います。

    テストが複雑になるため、更新ルール用のテストでは、フォーマットチェックテストは行っていません。

    ルールにフォーマットチェック項目を追加した後、テストの結果に影響がないかだけ確認したいと思います。

    以下のようにルールを調整してください。

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      function isTeacher(){
    19        return isUserRole("teacher");
    20      }
    21
    22      function isNotUpdate(key){
    23        return request.resource.data[key] == resource.data[key]
    24      }
    25
    26      function isHomeroomTeacher(){
    27        return request.auth.uid == resource.data.homeroomTeacher
    28      }
    29
    30      function isEvaluationPeriod(){
    31        return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod
    32      }
    33
    34      // 変更不可フィールドの不変チェック
    35      function isNotUpdateImmutables(){
    36        return isNotUpdate("id")
    37                && isNotUpdate("studentId")
    38                && isNotUpdate("season")
    39      }
    40
    41      function isValidFormat(){
    42        return request.resource.data.id is string
    43                && request.resource.data.studentId is string
    44                && request.resource.data.name is string
    45                && request.resource.data.season is string
    46                && request.resource.data.homeroomTeacher is string
    47                && request.resource.data.record is map
    48      }
    49
    50      allow update: if isAdmin()
    51                        && isNotUpdateImmutables()
    52                        && isNotUpdate("record")
    53                        && isValidFormat();
    54
    55      allow update: if isTeacher()
    56                        && isHomeroomTeacher()
    57                        && isNotUpdateImmutables()
    58                        && isNotUpdate("name")
    59                        && isNotUpdate("homeroomTeacher")
    60                        && isEvaluationPeriod()
    61                        && isValidFormat();
    62    }
    63  }
    64}

    create アクセス、update アクセスで共通して変更不可である idstudentIdseason フィールドの更新不可チェックを isNotUpdateImmutables() 関数にまとめました。

    また、admin ユーザと teacher ユーザのルールに、リクエストデータのフォーマットチェック関数 isValidFormat() を追加しました。

    テスト結果

    1 PASS  tests/data.update.test.ts
    2  成績データの更新テスト
    3ログインしていないユーザは更新不可 (95ms)
    4studentユーザは更新不可 (67ms)
    5    adminユーザの更新要件
    6要件にあったデータの更新に成功 (85ms)
    7idの更新不可 (75ms)
    8studentIdの更新不可 (70ms)
    9seasonの更新不可 (72ms)
    10recordの更新不可 (69ms)
    11    teacherユーザの更新要件
    12要件にあったデータの更新に成功 (65ms)
    13担任でないデータは更新不可 (98ms)
    14対象シーズンが成績評価期間外の場合は更新不可 (91ms)
    15      成績以外のフィールドは更新不可
    16idの更新不可 (63ms)
    17studentIdの更新不可 (62ms)
    18seasonの更新不可 (64ms)
    19nameの更新不可 (63ms)
    20homeroomTeacherの更新不可 (69ms)

    テスト結果に変化はありません。問題なく、ルールを調整・整理することができました。

    以上でデータ更新用ルールの実装は完了です。

    次のテストとの競合を避けるため、テストを終了してください。

    ここまでのルールの統合とリグレッションテスト

    最後に、今回実装したデータ更新用のルールと前回のデータ追加用のルールを統合します。

    ここまでのセキュリティルールを統合すると、以下のようになります。

    統合したセキュリティルール

    1rules_version = '2';
    2service cloud.firestore {
    3  match /databases/{database}/documents {
    4    match /students/{studentId}/records/{recordId}{
    5      function isLoggedIn(){
    6        return request.auth.uid != null;
    7      }
    8
    9      function isUserRole(role){
    10        return isLoggedIn()
    11                && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
    12      }
    13
    14      function isAdmin(){
    15        return isUserRole("admin");
    16      }
    17
    18      function isTeacher(){
    19        return isUserRole("teacher");
    20      }
    21
    22      function isNotUpdate(key){
    23        return request.resource.data[key] == resource.data[key]
    24      }
    25
    26      function isNotUpdateImmutables(){
    27        return isNotUpdate("id")
    28                && isNotUpdate("studentId")
    29                && isNotUpdate("season")
    30      }
    31
    32      function isHomeroomTeacher(){
    33        return request.auth.uid == resource.data.homeroomTeacher
    34      }
    35
    36      function isEvaluationPeriod(){
    37        return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod
    38      }
    39      
    40      function isRecordEmpty(_resource){
    41        return _resource.data.record == {}
    42      }
    43
    44      function isValidFormat(){
    45        return request.resource.data.id is string
    46                && request.resource.data.studentId is string
    47                && request.resource.data.name is string
    48                && request.resource.data.season is string
    49                && request.resource.data.homeroomTeacher is string
    50                && request.resource.data.record is map
    51      }
    52
    53      allow create: if isAdmin() 
    54                        && isRecordEmpty(request.resource)
    55                        && isValidFormat();
    56
    57      allow update: if isAdmin()
    58                        && isNotUpdateImmutables()
    59                        && isNotUpdate("record")
    60                        && isValidFormat();
    61
    62      allow update: if isTeacher()
    63                        && isHomeroomTeacher()
    64                        && isNotUpdateImmutables()
    65                        && isNotUpdate("name")
    66                        && isNotUpdate("homeroomTeacher")
    67                        && isEvaluationPeriod()
    68                        && isValidFormat();
    69    }
    70  }
    71}

    まとまりを考慮して、カスタム関数の順序を一部調整しています。

    全てのテストを実行

    以下のコマンドを実行し、ここまでの全てのテストを実行してください。

    1npm run test

    テスト結果

    1 PASS  tests/data.update.test.ts (6.081s)
    2 PASS  tests/data.create.test.ts
    3
    4Test Suites: 2 passed, 2 total
    5Tests:       21 passed, 21 total
    6Snapshots:   0 total
    7Time:        6.974s, estimated 8s
    8Ran all test suites.

    全てのテストに成功していますので、ここまでのルールが問題なく実装できていることが確認できました!

    第4回へつづく!

    「カスタム関数編」第3回となるこの記事では、成績コレクション records のデータ更新用セキュリティルールを実装しつつ、カスタム関数を使ってルールを整理してみました!

    次回は、成績コレクション records のデータ取得・削除用ルールの実装を進めます。

    データ取得・削除用ルールの実装した後、作成した全てのテストを実行し、作成したセキュリティルールが「カスタム関数編」第1回に設定した要件に沿ったルールとなっているかを確認してみます。

    次回もお楽しみに!

    第4回の記事はこちら

    featureImg2020.06.10テスト駆動で学ぶ Firestore セキュリティルール【カスタム関数編:第4回】【第4回】テスト駆動で成績データコレクションのルールを実装する前回の記事では、成績コレクション records のデー...

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

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

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

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

    広告メディア事業部

    広告メディア事業部

    おすすめ記事