🧑‍🚒

Firestore 公式ドキュメントを紐解く #2

10 min read

Cloud Firestore セキュリティ ルールの条件の記述

このガイドでは、セキュリティ・ルールの構造化ガイドをベースに、Cloud Firestoreセキュリティ・ルールに条件を追加する方法を説明します。Cloud Firestoreセキュリティ・ルールの基本に慣れていない場合は、スタート・ガイドを参照してください。

Cloud Firestoreセキュリティルールの主要な構成要素は '条件' です。
条件とは、特定の操作を許可するか拒否するかを決定するブール式のことです。
セキュリティルールを使用して、ユーザー認証のチェック、受信データの検証、またはデータベースの他の部分へのアクセスを行う条件を記述します。

認証

最も一般的なセキュリティルールのパターンの一つは、ユーザーの認証状態に基づいてアクセスを制御することです。

例) サインインしているユーザーだけにデータの読み書きを許可する

service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザーが認証された場合のみ
    // "cities "コレクションのドキュメントにアクセスできるようにします。
    match /cities/{city} {
      allow read, write: if request.auth != null;
    }
  }
}


もう一つのよくあるパターンは、ユーザーが自分のデータしか読み書きできないようにすることです。

例) ユーザー自身のデータのみ読み書きを許可する

service cloud.firestore {
  match /databases/{database}/documents {
    // 要求しているユーザーのuidがユーザードキュメントの名前と一致していることを確認してください。
    //ワイルドカード式{userId}は、userId変数をルールで使用できるようにします。
    match /users/{userId} {
      allow read, update, delete: if 
      request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null;
    }
  }
}

アプリが Firebase Authentication または Google Cloud Identity Platform を使用している場合、 request.auth 変数には、データを要求するクライアントの認証情報が格納されています。

request.auth の詳細についてはリファレンスドキュメントを参照してください。

.auth は以下の値を要求します

uid - リクエストしたユーザーのUIDです。

token - JWTトークンクレームのマップです。

トークン・マップには、以下の値が含まれます。

  • email
    アカウントに関連付けられたメールアドレス(存在する場合)。
    email_verified true は、ユーザーが email アドレスへのアクセス権を持っていることを確認した場合に表示されます。
    ( varifired = 検証済み)
  • phone_number アカウントに関連付けられている電話番号です(存在する場合)。
  • name ユーザーの表示名(設定されている場合)です。
  • sub ユーザーの Firebase UID です。これはプロジェクト内で一意です。
  • firebase.identities
    このユーザーのアカウントに関連付けられているすべての identitiesmapです。
    mapkeyは以下のいずれかになります。
    email, phone, google.com, facebook.com, github.com, twitter.com.
    mapの値は、アカウントに関連付けられている各IDプロバイダの一意の識別子のリストです。
    例えば、request.auth.token.firebase.identities["google.com"][0] には、アカウントに関連付けられた最初の Google ユーザー ID が含まれます。
  • firebase.sign_in_provider
    このトークンの取得に使用されるサインインプロバイダです。
    以下の文字列のいずれかになります。
    custom, password, phone, anonymous, google.com, facebook.com, github.com, twitter.com.

データの検証

多くのアプリケーションでは、データベース内のドキュメントのフィールドとしてアクセス制御情報が保存されています。
Cloud Firestoreのセキュリティ・ルールは、ドキュメント・データに基づいて、アクセスを動的に許可または拒否することができます。

例) 公開設定 ( visibility ) が公開 ( public ) に設定されているドキュメントを読み込めるようにする

service cloud.firestore {
  match /databases/{database}/documents {
    // ドキュメントの visibility フィールドが public に設定されている場合、
    // ユーザーにデータの読み取りを許可する
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
    }
  }
}

resource変数は、要求されたドキュメント、およびリソースを参照します。
data は、ドキュメントに格納されているすべてのフィールドと値のマップです。

resource変数の詳細については、リファレンスドキュメントを参照してください。

例) 不要なデータ更新や、矛盾したデータ更新を防ぐ

service cloud.firestore {
  match /databases/{database}/documents {
    // すべての cities の population が > 0 で、
    // name が変更されていない場合にだけ上書きを許可する。
    match /cities/{city} {
      allow update: if request.resource.data.population > 0
                    && request.resource.data.name == resource.data.name;
    }
  }
}

request.resourceのフィールド値をチェックすることで、不要なデータ更新や矛盾したデータ更新を防ぐことができます。

resource変数には、ドキュメントに上書きする内容が格納されます。
上記の場合、上書き内容がルールに合致している時にだけ書き込むことができます。

他のドキュメントへのアクセス

get() 関数と exists() 関数を使うことで、受信したリクエストをデータベース内の他のドキュメントと比較して評価することができます。

get()exists() 関数は、どちらも完全に指定されたドキュメントパスを想定しています。

exists () : boolean
DataSnapshot にデータが含まれている場合に true を返します。
snapshot.val() !== null を使うよりも若干効率的です。

get()exists() で変数を使ったパスを作成する場合は、変数を $() で囲ってエスケープする必要があります。

例) 管理者 ( Adim ) が 一般ユーザー のテータを削除できるようにする

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      // cities コレクションへの書き込みを許可する前に、
      // リクエストしたユーザーの users ドキュメントが存在することを確認します。
      allow create: if request.auth != null && 
      exists(/databases/$(database)/documents/users/$(request.auth.uid))

      // ユーザードキュメントの admin フィールドが true に設定されている場合、
      // ユーザーが都市を削除できるようにする。
      allow delete: if request.auth != null && 
      get(/databases/$(database)/documents/users/$(request.auth.uid))
      .data.admin == true
    }
  }
}

書き込みの場合、トランザクション や バッチが完了した後、処理が行われる前のドキュメントの状態にアクセスするには、getAfter() 関数を使用します。

getAfter() で消去したデータの復元ができると思われます。

トランザクション: 複数のドキュメントに対して読み取り、書き込みをおこなうこと。
バッチ: 複数のドキュメントに対して書き込みだけをおこなうこと。

get() と同様に、 getAfter() 関数は、完全に指定されたドキュメントパスを受け取ります。
getAfter() を使用して、 トランザクション や バッチ として一緒に行わなければならない条件を定義することができます。

パスの作成の仕方は get() と同じだということが言いたいと思われます。

アクセスの制限

ルールセット評価ごとのドキュメントアクセスコールには制限があります。

  • ひとつのドキュメント、及びクエリの呼び出しでは10。
  • マルチドキュメントの読み取り、トランザクション、およびバッチについては20。
    前述の制限値である10も含まれます。

いずれかの制限を超えると、許可が得られないというエラーになります。
ドキュメントのアクセスコールの中にはキャッシュされているものがありますが、キャッシュされたコールは制限の対象にはなりません。

アクセスと料金

これらの関数を使用すると、お客様のデータベースで読み取り操作が実行されます。
つまり、お客様のルールでリクエストが拒否された場合でも、ドキュメントの読み取りに対する請求が発生します。
より具体的な請求情報については、Cloud Firestoreの価格を参照してください。

カスタム関数

セキュリティルールが複雑になってくると、ルールセット全体で再利用できるように、条件のセットを関数にまとめることが必要になってきます。
セキュリティルールはカスタム関数をサポートしています。
カスタム関数の構文はJavaScriptに似ていますが、セキュリティルールの関数はドメイン固有の言語で書かれており、いくつかの重要な制限があります。

なんと、セキュリティールールで使用されている言語は JavaScript ではないとのこと。

  • 関数は、1つのreturn文のみを含むことができます。
    追加のロジックを含めること(ループの実行や外部サービスの呼び出し)はできません。

  • 関数は、定義されているスコープ内の関数や変数に自動的にアクセスできます。
    たとえば、以下の service cloud.firestore のスコープで定義された関数は、 resource 変数と get()exists() などの組み込み関数にアクセスできます。

  • 関数は他の関数を呼び出すことができますが、再帰はできません。呼び出しスタックの深さの合計は10に制限されています。

  • rules version v2 では、関数は let を使って変数を定義できます。
    関数は最大 10 個の let を持つことができますが、必ず return 文で終わらなければなりません。

関数は、 function キーワードで定義されます。
例えば、上記の例で使用した2種類の条件を1つの関数にまとめることができます。

セキュリティルールに関数を使用することで、ルールが複雑になってもメンテナンス性が向上します。

service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザーがサインインしている場合や、要求されたデータが 'public' の場合に `true`
    function signedInOrPublic() {
      return request.auth.uid != null || resource.data.visibility == 'public';
    }

    match /cities/{city} {
      allow read, write: if signedInOrPublic();
    }

    match /users/{user} {
      allow read, write: if signedInOrPublic();
    }
  }
}

セキュリティールールはフィルタではない

データを保護してクエリを書き始めたら、セキュリティ・ルールはフィルタではないことに注意してください。
コレクション内のすべてのドキュメントに対するクエリを書くことはできません。
Cloud Firestore によって返されるのは、現在のクライアントのアクセス権が設定されているドキュメントだけです。

例えば、次のようなセキュリティルールがあるとします。

service cloud.firestore {
  match /databases/{database}/documents {
    // ドキュメントの 'visibility' が 'public' に設定されている場合、
    // ユーザーにデータの読み取りを許可する
    match /cities/{city} {
      allow read: if resource.data.visibility == 'public';
    }
  }
}

❌ このルールでは、visibilitypublic でないドキュメントが含まれる可能性があるため、次のクエリを拒否します。

db.collection("cities").get()
    .then(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
            console.log(doc.id, " => ", doc.data());
    });
});

✅ このルールでは、where("visibility", "==", "public") によりルール条件を満たすことを保証するため、以下のクエリを許可します。

db.collection("cities").where("visibility", "==", "public").get()
    .then(function(querySnapshot) {
        querySnapshot.forEach(function(doc) {
            console.log(doc.id, " => ", doc.data());
        });
    });

Cloud Firestore セキュリティ ルールは、各クエリをその結果の可能性と比較して評価し、クライアントが読み取り権限を持たないドキュメントを返す可能性がある場合はリクエストを拒否します。

クエリは、セキュリティ ルールで設定された制約に従う必要があります。

セキュリティルールとクエリの詳細については、「データを安全にクエリする」を参照してください。

Cloud Firestore セキュリティ ルールの条件の記述 のチャプターはここまで