
テスト駆動で学ぶ Firestore セキュリティルール 【カスタム関数編:第3回】
2021.12.20
【第3回】テスト駆動で成績データコレクションのルールを実装する
前回の記事では、成績コレクション records のデータ追加ルールを実装すると共に、カスタム関数を利用したルールの整理について解説してみました。
今回は、成績コレクション records のデータ更新ルールの実装を進めると共に、引き続き、カスタム関数を利用したルールの整理について解説していきます。
前回の記事はこちら
前回の記事は、こちらをご参照ください。
Firestore 上のデータ更新ルールの実装について
テストの作成前に、まずは、データ更新の要件を再確認します。
admin ユーザの更新要件
- id の更新不可
- studentId の更新不可
- season の更新不可
- record の更新不可
teacher ユーザの更新要件
- 担任でないデータは更新不可
- 成績以外のフィールドは更新不可
- 対象シーズンが成績評価期間外の場合は更新不可
その他の更新要件
- student ユーザは更新不可
- 所定のフォーマットでないデータは更新不可
上記の要件に沿って、テストを作成していきます。
tests/data.update.test.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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | process.env.FIRESTORE_EMULATOR_HOST = "localhost:58080"; import { FirestoreTestSupporter } from "firestore-test-supporter"; import * as path from "path"; import * as firebase from "@firebase/testing"; import { users } from "./data/collections/users"; import { records } from "./data/collections/records"; import { seasons } from "./data/collections/seasons"; import InitialData from "./data/InitialData"; describe("成績データの更新テスト", () => { const rulesFilePath = path.join(__dirname, "firestore.rules"); const supporter = new FirestoreTestSupporter("my-test-project", rulesFilePath); beforeEach(async () => { await supporter.loadRules(); // 各コレクションに初期データを追加 const initialData = new InitialData(rulesFilePath); await initialData.setupUsers(); await initialData.setupRecords(); await initialData.setupSeasons(); }); afterEach(async () => { await supporter.cleanup() }); test('ログインしていないユーザは更新不可', async () => { // 認証されていないクライアントを取得 const db = supporter.getFirestore(); const doc = db.collection(records.collectionPath).doc(records.initialData.id); await firebase.assertFails(doc.update(records.initialData)) }); describe("adminユーザの更新要件", () => { let doc: firebase.firestore.DocumentReference; beforeEach(() => { // adminユーザで認証されたクライアントを取得 const db = supporter.getFirestoreWithAuth(users.sample.admin); doc = db.collection(records.collectionPath).doc(records.initialData.id); }); test('要件にあったデータの更新に成功', async () => { await firebase.assertSucceeds(doc.update(records.validUpdateDataForAdmin)); }); test('idの更新不可', async () => { // リクエストデータのidを変更 const badData = { ...records.validUpdateDataForAdmin, id: "other_id" }; await firebase.assertFails(doc.update(badData)); }); test('studentIdの更新不可', async () => { // リクエストデータのstudentIdを変更 const badData = { ...records.validUpdateDataForAdmin, studentId: "other0000" }; await firebase.assertFails(doc.update(badData)); }); test('seasonの更新不可', async () => { // リクエストデータのseasonを変更 const badData = { ...records.validUpdateDataForAdmin, season: { year: 2011, semester: 3 } }; await firebase.assertFails(doc.update(badData)); }); test('recordの更新不可', async () => { // リクエストデータのrecordを変更 const badData = { ...records.validUpdateDataForAdmin, record: { Math: 3, NationalLanguage: 4 } }; await firebase.assertFails(doc.update(badData)); }); }); describe("teacherユーザの更新要件", () => { let doc: firebase.firestore.DocumentReference; beforeEach(() => { // teacherユーザで認証されたクライアントを取得 const db = supporter.getFirestoreWithAuth(users.sample.teacher); doc = db.collection(records.collectionPath).doc(records.initialData.id); }); test('要件にあったデータの更新に成功', async () => { await firebase.assertSucceeds(doc.update(records.validUpdateDataForTeacher)); }); test('担任でないデータは更新不可', async () => { // データベース上のデータの担任を変更 const dbWithAdmin = supporter.getAdminFirestore(); const docWithAdmin = dbWithAdmin.collection(records.collectionPath).doc(records.initialData.id); const newData = { ...records.validUpdateDataForTeacher, homeroomTeacher: "other_teacher" }; await firebase.assertSucceeds(docWithAdmin.update(newData)); await firebase.assertFails(doc.update(records.validUpdateDataForTeacher)); }); describe('成績以外のフィールドは更新不可', () => { test('idの更新不可', async () => { // リクエストデータのidを変更 const badData = { ...records.validUpdateDataForTeacher, id: "other_id" }; await firebase.assertFails(doc.update(badData)); }); test('studentIdの更新不可', async () => { // リクエストデータのstudentIdを変更 const badData = { ...records.validUpdateDataForTeacher, studentId: "other0000" }; await firebase.assertFails(doc.update(badData)); }); test('seasonの更新不可', async () => { // リクエストデータのseasonを変更 const badData = { ...records.validUpdateDataForTeacher, season: { year: 2009, semester: 1 } }; await firebase.assertFails(doc.update(badData)); }); test('nameの更新不可', async () => { // リクエストデータのnameを変更 const badData = { ...records.validUpdateDataForTeacher, name: "other_name" }; await firebase.assertFails(doc.update(badData)); }); test('homeroomTeacherの更新不可', async () => { // リクエストデータのhomeroomTeacherを変更 const badData = { ...records.validUpdateDataForTeacher, homeroomTeacher: "other_id" }; await firebase.assertFails(doc.update(badData)); }); }); test('対象シーズンが成績評価期間外の場合は更新不可', async () => { // データベース上のデータの成績評価期間フラグをfalseに変更 const dbWithAdmin = supporter.getAdminFirestore(); const seasonDoc = dbWithAdmin.collection(seasons.collectionPath).doc(seasons.initialData(true).id); await firebase.assertSucceeds(seasonDoc.update(seasons.initialData(false))); await firebase.assertFails(doc.update(records.validUpdateDataForTeacher)); }); }); test('studentユーザは更新不可', async () => { // studentユーザで認証されたクライアントを取得 const db = supporter.getFirestoreWithAuth(users.sample.student); const doc = db.collection(records.collectionPath).doc(records.initialData.id); await firebase.assertFails(doc.update(records.initialData)); }); }); |
テストの概要
「admin ユーザの更新要件」テスト
beforeEach() 関数内で、各テスト前に admin ユーザで認証されたクライアントを取得しています。
「teacher ユーザの更新要件」テスト
adminユーザの更新要件 テストと同様に、teacher ユーザで認証されたクライアントを取得しています。
担任でないデータは更新不可 となるテストでは、データ更新前に、データベース上のデータの担任を、リクエストデータの担任とは異なる担任に変更。
対象シーズンが成績評価期間外の場合は更新不可 となるテストでは、データ更新の前に、 seasons コレクションの対象となる、 season ドキュメントの成績評価期間フラグを「false」に変更しています。
「所定のフォーマットでないデータは更新不可」となる要件について
データ更新については、フォーマットチェックテストが複雑になるため、解説の便宜上、テストを作成していません。
リクエストデータのフォーマットは、データ追加の場合と同じフォーマットを要件としています。
フォーマットチェックには、前回の記事で定義した、フォーマットチェック用関数 isValidFormat() を使用。
他のテストの結果に、影響がないかどうかだけを確認します。
Firestore 上のデータ更新ルールを実装してみる
それでは、作成したテストに沿って、セキュリティルールを実装していきましょう。
セキュリティルールが少し長くなってきているので、ここでは、前回の記事で実装したルールとは分けて、データ更新のルールだけを実装していきます。
そして、最後に、データ追加用のルールとデータ更新用のルールを統合したいと思います。
全ての update アクセスを許可するルールを設定
データ追加ルールの実装と同様、まずは、 records コレクションの update アクセスを全て許可し、「要件にあったデータの追加に成功」するテストを通します。
その後、失敗したテストを順次、上から通していきます。
セキュリティルールの調整
以下のように、セキュリティルールを調整してください。
1 2 3 4 5 6 7 8 9 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ // 全てのupdateアクセスを許可 allow update: if true; } } } |
テストをスタート
ルールを調整したら、以下のコマンドを実行して、データ更新ルール用のテストをスタートしてください。
1 | npm run test-watch tests/data.update.test.ts |
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | FAIL tests/data.update.test.ts 成績データの更新テスト × ログインしていないユーザは更新不可 (1171ms) × studentユーザは更新不可 (64ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (87ms) × idの更新不可 (87ms) × studentIdの更新不可 (68ms) × seasonの更新不可 (71ms) × recordの更新不可 (66ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (63ms) × 担任でないデータは更新不可 (1152ms) × 対象シーズンが成績評価期間外の場合は更新不可 (99ms) 成績以外のフィールドは更新不可 × idの更新不可 (70ms) × studentIdの更新不可 (64ms) × seasonの更新不可 (65ms) × nameの更新不可 (71ms) × homeroomTeacherの更新不可 (67ms) |
admin ユーザと teacher ユーザに対して、 要件にあったデータの更新に成功 するテストが成功しました。
続いて、失敗しているテストを上から順に通していきたいと思います。
「admin, teacher ユーザ以外は更新不可」となるルールを設定
ログインしていないユーザは更新不可、 studentユーザは更新不可 となるように、admin ユーザか teacher ユーザでない場合は、更新不可となるルールを追加します。
データ追加ルールの実装で定義した isAdmin() 関数と同様、新しく isTeacher() 関数を定義します。
以下のようにルールを調整してください。
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 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } // teacherロールチェック function isTeacher(){ return isUserRole("teacher"); } allow update: if isAdmin(); allow update: if isTeacher(); } } } |
ここで、 isTeacher() 関数では、データ追加ルールの実装の最後で定義したロールチェック用関数 isUserRole() を使用し、リクエストユーザが teacher ロールを持っているかどうかをチェックしています。
isAdmin() 関数と同様、 isTeacher() 関数にはログインチェックも含んでいます。
また、admin ユーザと teacher ユーザで要件が異なるため、以降のルール調整に備えて、ルールを admin ユーザと teacher ユーザで分けて記述しました。
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | FAIL tests/data.update.test.ts 成績データの更新テスト √ ログインしていないユーザは更新不可 (154ms) √ studentユーザは更新不可 (72ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (109ms) × idの更新不可 (121ms) × studentIdの更新不可 (92ms) × seasonの更新不可 (81ms) × recordの更新不可 (87ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (1247ms) × 担任でないデータは更新不可 (130ms) × 対象シーズンが成績評価期間外の場合は更新不可 (91ms) 成績以外のフィールドは更新不可 × idの更新不可 (69ms) × studentIdの更新不可 (72ms) × seasonの更新不可 (83ms) × nameの更新不可 (77ms) × homeroomTeacherの更新不可 (84ms) |
ログインしていないユーザは更新不可、 studentユーザは更新不可 となるテストが通りました。
「admin ユーザの更新要件」をルールに設定
続いて、 adminユーザの更新要件 の更新不可テストを、まとめて通してしまいます。
以下のように、ルールを調整してください。
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 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } function isTeacher(){ return isUserRole("teacher"); } // 指定フィールドの更新なしチェック function isNotUpdate(key){ return request.resource.data[key] == resource.data[key] } allow update: if isAdmin() && isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") && isNotUpdate("record"); allow update: if isTeacher(); } } } |
request.resource.data.some_field == resource.data.some_field のように記述することで、対象フィールドを変更不可とすることができます。
どのフィールドでも同様の記述となるので、重複を避けるために isNotUpdate() 関数を定義しています。
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | FAIL tests/data.update.test.ts 成績データの更新テスト √ ログインしていないユーザは更新不可 (1160ms) √ studentユーザは更新不可 (69ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (82ms) √ idの更新不可 (80ms) √ studentIdの更新不可 (67ms) √ seasonの更新不可 (73ms) √ recordの更新不可 (68ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (63ms) × 担任でないデータは更新不可 (94ms) × 対象シーズンが成績評価期間外の場合は更新不可 (96ms) 成績以外のフィールドは更新不可 × idの更新不可 (65ms) × studentIdの更新不可 (68ms) × seasonの更新不可 (60ms) × nameの更新不可 (1122ms) × homeroomTeacherの更新不可 (69ms) |
adminユーザの更新要件 テストが全て通ったので、これで admin ユーザに対するルール設定は完了です。
「teacher ユーザの更新要件」をルールに設定
続いて、 teacherユーザの更新要件 テストを通して行きます。
担任でないデータは更新不可、 対象シーズンが成績評価期間外の場合は更新不可 となるよう、以下のようにルールを調整してください。
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 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } function isTeacher(){ return isUserRole("teacher"); } function isNotUpdate(key){ return request.resource.data[key] == resource.data[key] } // 担任チェック function isHomeroomTeacher(){ return request.auth.uid == resource.data.homeroomTeacher } // 成績評価期間チェック function isEvaluationPeriod(){ return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod } allow update: if isAdmin() && isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") && isNotUpdate("record"); allow update: if isTeacher() && isHomeroomTeacher() && isEvaluationPeriod(); } } } |
ここで、 isHomeroomTeacher() 関数により、リクエストユーザが対象となる成績データの担任であるかどうかを判定しています。
isEvaluationPeriod() 関数は、 seasons コレクションの対象となる season ドキュメントを参照して、成績評価期間フラグをチェックしています。
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | FAIL tests/data.update.test.ts (5.181s) 成績データの更新テスト √ ログインしていないユーザは更新不可 (2122ms) √ studentユーザは更新不可 (64ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (94ms) √ idの更新不可 (76ms) √ studentIdの更新不可 (75ms) √ seasonの更新不可 (71ms) √ recordの更新不可 (62ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (68ms) √ 担任でないデータは更新不可 (91ms) √ 対象シーズンが成績評価期間外の場合は更新不可 (86ms) 成績以外のフィールドは更新不可 × idの更新不可 (1108ms) × studentIdの更新不可 (72ms) × seasonの更新不可 (69ms) × nameの更新不可 (63ms) × homeroomTeacherの更新不可 (68ms) |
担任でないデータは更新不可 となるテストと、 対象シーズンが成績評価期間外の場合は更新不可 となるテストが通りました。
「成績以外のフィールドは更新不可」となるルールを設定
最後に、 成績以外のフィールドは更新不可 となるテストをまとめて通してしまいます。
先ほど定義した isNotUpdate() 関数を使って、以下のようにルールを調整してください。
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 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } function isTeacher(){ return isUserRole("teacher"); } function isNotUpdate(key){ return request.resource.data[key] == resource.data[key] } function isHomeroomTeacher(){ return request.auth.uid == resource.data.homeroomTeacher } function isEvaluationPeriod(){ return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod } allow update: if isAdmin() && isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") && isNotUpdate("record"); allow update: if isTeacher() && isHomeroomTeacher() && isEvaluationPeriod() && isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") && isNotUpdate("name") && isNotUpdate("homeroomTeacher"); } } } |
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | PASS tests/data.update.test.ts 成績データの更新テスト √ ログインしていないユーザは更新不可 (100ms) √ studentユーザは更新不可 (91ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (89ms) √ idの更新不可 (81ms) √ studentIdの更新不可 (84ms) √ seasonの更新不可 (70ms) √ recordの更新不可 (72ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (70ms) √ 担任でないデータは更新不可 (88ms) √ 対象シーズンが成績評価期間外の場合は更新不可 (90ms) 成績以外のフィールドは更新不可 √ idの更新不可 (62ms) √ studentIdの更新不可 (63ms) √ seasonの更新不可 (63ms) √ nameの更新不可 (75ms) √ homeroomTeacherの更新不可 (70ms) |
全てのテストが通ったので、これで要件に沿ったデータ更新用のルールを実装することができました。
カスタム関数でルールを整理
データ追加ルールの実装のときと同じように、今回もテスト環境を活かしてルールをもう少しだけ調整・整理したいと思います。
isNotUpdateImmutables() 関数を定義
record ドキュメントのフォーマット定義で変更不可としたフィールドは、当然ながら、admin ユーザと teacher ユーザの両方が更新不可となります。
このため、「allow式」中で、変更不可チェック用に追加した条件が重複しています。
これらのフィールドの不変チェックをひとまとめにして、 isNotUpdateImmutables() 関数を定義します。
リクエストデータのフォーマットチェック
また、リクエストデータのフォーマットは、データ追加の場合と同じフォーマットです。
そのため、データ追加ルールの実装時に定義した isValidFormat() 関数を使い、リクエストデータのフォーマットチェックを行います。
テストが複雑になるため、更新ルール用のテストでは、フォーマットチェックテストは行っていません。
ルールにフォーマットチェック項目を追加した後、テストの結果に影響がないかだけ確認したいと思います。
以下のようにルールを調整してください。
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 52 53 54 55 56 57 58 59 60 61 62 63 64 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } function isTeacher(){ return isUserRole("teacher"); } function isNotUpdate(key){ return request.resource.data[key] == resource.data[key] } function isHomeroomTeacher(){ return request.auth.uid == resource.data.homeroomTeacher } function isEvaluationPeriod(){ return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod } // 変更不可フィールドの不変チェック function isNotUpdateImmutables(){ return isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") } function isValidFormat(){ return request.resource.data.id is string && request.resource.data.studentId is string && request.resource.data.name is string && request.resource.data.season is string && request.resource.data.homeroomTeacher is string && request.resource.data.record is map } allow update: if isAdmin() && isNotUpdateImmutables() && isNotUpdate("record") && isValidFormat(); allow update: if isTeacher() && isHomeroomTeacher() && isNotUpdateImmutables() && isNotUpdate("name") && isNotUpdate("homeroomTeacher") && isEvaluationPeriod() && isValidFormat(); } } } |
create アクセス、 update アクセスで共通して変更不可である id、 studentId、 season フィールドの更新不可チェックを isNotUpdateImmutables() 関数にまとめました。
また、admin ユーザと teacher ユーザのルールに、リクエストデータのフォーマットチェック関数 isValidFormat() を追加しました。
テスト結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | PASS tests/data.update.test.ts 成績データの更新テスト √ ログインしていないユーザは更新不可 (95ms) √ studentユーザは更新不可 (67ms) adminユーザの更新要件 √ 要件にあったデータの更新に成功 (85ms) √ idの更新不可 (75ms) √ studentIdの更新不可 (70ms) √ seasonの更新不可 (72ms) √ recordの更新不可 (69ms) teacherユーザの更新要件 √ 要件にあったデータの更新に成功 (65ms) √ 担任でないデータは更新不可 (98ms) √ 対象シーズンが成績評価期間外の場合は更新不可 (91ms) 成績以外のフィールドは更新不可 √ idの更新不可 (63ms) √ studentIdの更新不可 (62ms) √ seasonの更新不可 (64ms) √ nameの更新不可 (63ms) √ homeroomTeacherの更新不可 (69ms) |
テスト結果に変化はありません。問題なく、ルールを調整・整理することができました。
以上でデータ更新用ルールの実装は完了です。
次のテストとの競合を避けるため、テストを終了してください。
ここまでのルールの統合とリグレッションテスト
最後に、今回実装したデータ更新用のルールと前回のデータ追加用のルールを統合します。
ここまでのセキュリティルールを統合すると、以下のようになります。
統合したセキュリティルール
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /students/{studentId}/records/{recordId}{ function isLoggedIn(){ return request.auth.uid != null; } function isUserRole(role){ return isLoggedIn() && role in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles; } function isAdmin(){ return isUserRole("admin"); } function isTeacher(){ return isUserRole("teacher"); } function isNotUpdate(key){ return request.resource.data[key] == resource.data[key] } function isNotUpdateImmutables(){ return isNotUpdate("id") && isNotUpdate("studentId") && isNotUpdate("season") } function isHomeroomTeacher(){ return request.auth.uid == resource.data.homeroomTeacher } function isEvaluationPeriod(){ return get(/databases/$(database)/documents/seasons/$(resource.data.season)).data.evaluationPeriod } function isRecordEmpty(_resource){ return _resource.data.record == {} } function isValidFormat(){ return request.resource.data.id is string && request.resource.data.studentId is string && request.resource.data.name is string && request.resource.data.season is string && request.resource.data.homeroomTeacher is string && request.resource.data.record is map } allow create: if isAdmin() && isRecordEmpty(request.resource) && isValidFormat(); allow update: if isAdmin() && isNotUpdateImmutables() && isNotUpdate("record") && isValidFormat(); allow update: if isTeacher() && isHomeroomTeacher() && isNotUpdateImmutables() && isNotUpdate("name") && isNotUpdate("homeroomTeacher") && isEvaluationPeriod() && isValidFormat(); } } } |
※ まとまりを考慮して、カスタム関数の順序を一部調整しています。
全てのテストを実行
以下のコマンドを実行し、ここまでの全てのテストを実行してください。
1 | npm run test |
テスト結果
1 2 3 4 5 6 7 8 | PASS tests/data.update.test.ts (6.081s) PASS tests/data.create.test.ts Test Suites: 2 passed, 2 total Tests: 21 passed, 21 total Snapshots: 0 total Time: 6.974s, estimated 8s Ran all test suites. |
全てのテストに成功していますので、ここまでのルールが問題なく実装できていることが確認できました!
第4回へつづく!
「カスタム関数編」第3回となるこの記事では、成績コレクション records のデータ更新用セキュリティルールを実装しつつ、カスタム関数を使ってルールを整理してみました!
次回は、成績コレクション records のデータ取得・削除用ルールの実装を進めます。
データ取得・削除用ルールの実装した後、作成した全てのテストを実行し、作成したセキュリティルールが「カスタム関数編」第1回に設定した要件に沿ったルールとなっているかを確認してみます。
次回もお楽しみに!
第4回の記事はこちら
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の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世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン