iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

Cloud Firestore でセキュリティールールを作ってテストするまで

セキュリティルールとは?

Cloud Firestore のドキュメント DB に関してかけられる制約のことです。これによってサーバ側の認証・承認などのコードを作成する必要がなくなります。また、認証という役割だけではなく DB に対するデータの制約なんかもこのセキュリティルールで決めることができるので、Cloud Firestore の設計においては最も重要な部分と行っても過言ではないでしょう。

ルールの基本記述

セキュリティールールはデータベース内のドキュメントを識別する match ステートメントと、アクセスを許可する allow 式で構成されます。また、モバイル・Web の Firestore SDK から送信される全てのリクエストはデータの読み取り・書き込み前にセキュリティルールの確認がされ、ドキュメントパスへのアクセスがルールによって拒否されるとリクエスト全体が失敗します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /<some_path>/ {
      allow read, write: if <some_condition>;
    }
  }
}

アクセス権は主に2つの種類があり、それぞれ下記のように細分化できます。

  • read
    • get
    • list
  • write
    • create
    • update
    • delete

ルールの作成からテストの流れ

今回は下記のような流れで、セキュリティルールの作成からテストまでを行っていきたいと思います。また、Firebase プロジェクトを作成する部分は割愛しています。

  1. Firebase プロジェクトの作成(割愛)
  2. FirebaseCLI を使用して既存の Firebase プロジェクトをローカル環境で編集できるようにセットアップ
  3. セキュリティルールを書く
  4. セキュリティルールテストに必要なセットアップ
  5. セキュリティルールテストを書く
  6. テスト実行

作業開始👨‍💻

2. Firebase プロジェクトをローカル環境で編集できるようにセットアップ

まずは適当にディレクトリを作って作業場に移動します。

$ mkdir firestore-sandbox
$ cd firestore-sandbox

下記コマンドで、firebase の firestore 機能だけを初期化します。

$ firebase init firestore

すると下記のような画面になり、どの Firebase Project を使用するのか聞かれるので Use an existing project を選択してあらかじめ作成しておいた Firebase プロジェクトを指定します。

f:id:yum_fishing:20200812214334p:plain

次に進むと、Firestore のルールを定義するファイルになにを指定するかを聞かれるので Enter でデフォルト(firestore.rules)に設定にします。

? What file should be used for Firestore Rules?

次に進むと、同じように Firestore のインデックスを定義するファイルになにを指定するかを聞かれるので Enter でデフォルト(firestore.indexes.json)に設定します。

? What file should be used for Firestore indexes?

これで Firestore のセキュリティルールを書くためのセットアップは完了です。

3. セキュリティールールを書く

今回はシンプルに User と Admin テーブルだけ存在する DB を仮定してルールを書いていきます。仕様は下記のように定義します。

  • User

    • read
      • get: 認証済みのユーザ
      • list: 認証済みのユーザ
    • write:
      • create: 認証済みのユーザかつ userId と authId が同じユーザ
      • update: 認証済みのユーザかつ userId と authId が同じユーザ・Admin ユーザ
      • delete: Admin ユーザ
  • Admin

    • read: Admin ユーザ

そしてルールとして定義した firestore.rules は下記のようになります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 認証済みのユーザかどうか?
    function isAuthenticated() {
      return request.auth != null;
    }
    // 指定した userId とリクエストしてきた uid が同じかどうか?
    function isMe(userId) {
      return request.auth.uid == userId;
    }
    // リクエストしてきたユーザが Admin かどうか?
    // 参考: https://medium.com/google-cloud-jp/firestore3-9518331f8748
    function isAdmin() {
      return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
    }

    match /users/{userId} {
      allow read: if isAuthenticated();
      allow create: if isMe(userId);
      allow update: if isMe(userId) || isAdmin();
      allow delete: if isAdmin();
    }

    match /admins/{adminId} {
      allow read: if isAdmin();
    }
  }
}

基本的には、細分化して定義するルール以外は read, write で定義します。また、アクセス権が定義されていない場合はデフォルトでアクセスができなくなります。今回の場合は、Admin の write はアクセスできない状態に設定してあります。

これでルールの記述も完了したので、テストをしていきたいと思います。

4. セキュリティルールテストに必要なセットアップ

ざっくりと必要なものは下記になります。テスティングフレームワークは Jest を使った記事も多く見かけましたが、公式で見つけたサンプル で使用されている mocha で行います。

  • npm でインストールするもの
    • @firebase/testing
    • firebase-tools
    • mocha
  • FirebaseCLI でインストールするもの

まずは npm で必要なものをインストールするために、npm を初期化して、とりあえず全部エンターで進めます。npm がインストール済みでない方はこちらの記事を見てみてください。

$ npm init

初期化できたら、package.json を開いて scripts と devDependencies を下記のように変更していきます。

{
  "name": "firestore-sandbox",
  "version": "1.0.0",
  "scripts": {
    "test": "mocha test/*.test.js"
  },
  "license": "Apache-2.0",
  "devDependencies": {
    "@firebase/testing": "^0.20.5",
    "firebase-tools": "^8.4.3",
    "mocha": "^8.0.1"
  }
}

変更できたら下記コマンドで必要なツールをインストールします。

$ npm i

npm でそれぞれのパッケージがインストールできたら、次は Firestore エミュレータ をインストールしていきます。下記のコマンドでインストールしましょう。

$ firebase setup:emulators:firestore

Firestore エミュレータ のインストールが完了したら、テストを記述するための js ファイルを作成しておきます。{ファイル名}.test.js のような命名にすることで、テストを実行する際に package.json で定義した mocha のスクリプトがファイルを読み込めるようになります。

$ mkdir test
$ touch test/firestore.test.js

5. セキュリティルールテストを書く

今回は下記の4パターンのテストを追加したいと思います。

  • Get-User
    • auth: null => fails
    • auth: not null => success
  • Create-User(id: abcd)
    • auth.uid: ffff => fails
    • auth.uid: abcd => success

そして、下記が今回作成したテストになります。

const firebase = require("@firebase/testing");
const fs = require("fs");
// FirebaseプロジェクトIDを入れます
const PROJECT_ID = "your_project_id";

function getAuthedFirestore(auth) {
    return firebase
    .initializeTestApp({ projectId: PROJECT_ID, auth })
    .firestore();
}

beforeEach(async () => {
    // Firestore エミュレータは一回のテスト実行でデータを保持するため、次のテストに影響がでないように `clearFirestoreData()` でデータをクリアします。
    await firebase.clearFirestoreData({ projectId: PROJECT_ID });
});

before(async () => {
    // セキュリティールールを読み込みます。
    const rules = fs.readFileSync("firestore.rules", "utf8");
    await firebase.loadFirestoreRules({ projectId: PROJECT_ID, rules });
});

after(async () => {
    // 初期化済みのアプリでアクティブなリスナーを持つ場合、JavaScript は終了しないので、アクティブなリスナーをテスト後に削除します。
    await Promise.all(firebase.apps().map((app) => app.delete()));
});

describe("Firebase SandBox", () => {
    it("Get request fails if auth is null", async() => {
        const db = getAuthedFirestore(null);
        const profile = db.collection("users").doc("abcd");
        await firebase.assertFails(profile.get());
    });
    it("Get request success if auth is not null", async() => {
        const db = getAuthedFirestore({ uid: "ffff" });
        const profile = db.collection("users").doc("abcd");
        await firebase.assertSucceeds(profile.get());
    });
    it("Create reqest fails if auth uid and userId not match", async() => {
        const db = getAuthedFirestore({ uid: "ffff" });
        const profile = db.collection("users").doc("abcd");
        await firebase.assertFails(profile.set({ name: "hoge taro" }));
    });
    it("Create reqest success if auth uid and userId match", async() => {
        const db = getAuthedFirestore({ uid: "abcd" });
        const profile = db.collection("users").doc("abcd");
        await firebase.assertSucceeds(profile.set({ name: "hoge taro" }));
    });
});

これでテストの作成が完了したので、実際に実行してみます。

6. テスト実行

テストは Firestore エミュレータを起動した状態で行うことで、ローカル環境に Firebase の擬似的なシステムを作り、単体テストを行うことができます。早速下記プロジェクトのルートで下記コマンドを叩いてエミュレータを起動させましょう。

$ firebase emulators:start --only firestore

Firestore エミュレータが起動したら別でターミナルを開くなりして、プロジェクトルートで下記コマンドを叩き、テストを実行します。

$  npm test

すると下記のように4つのテストが全て成功したのが分かります🎉🎉

f:id:yum_fishing:20200812235008p:plain

参考

cloud.google.com

github.com

medium.com

tech-blog.sgr-ksmt.org

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com