uwu

プログラミングの備忘録を書いています。誰かの為になれば幸いです

Firestore Cloud firestoreの公式動画まとめ6

前回の記事の続きです。
今回はセキュリティルールについてのお話です!!



www.youtube.com
※この記事で使われている全ての画像はこの動画内から切り抜いたものです。


セキュリティルールについて

もし、あなたがクライエントにDBへ接続するための
アクセス制限のないAPIキーをあげていたとしたら
悪意のあるユーザーは簡単にあなたのDBへ攻撃することができます。



セキュリティルールを使用すると、データベース内のドキュメント
およびコレクションへのアクセスを制御できます。
Firestoreではこのセキュリティルールの設定が重要となります。


クライエントとセキュリティ

クライエントからのリクエストがいつも正しいと信じるべきではない。



あなたがとても良くバリデーションをしたクライエントサイドのコードを
書いていて、テストを行っていたとしてもクライエントは
簡単にクライエントリクエストを変えたり虚偽のリクエストを送ることができます。



データを守るためには、クライエントが正しい行動を
とらないことを前提として考える必要があります。



セキュリティルールはどのように動くのか


Firestoreに送られてくるリクエストは、CRUDのいずれかの操作を行うか、
コレクションから大量のドキュメントを取得しようとしているリクエストです。
リクエストが送られてきたとき、
Firestoreは、指定されたドキュメントに適用される一連のセキュリティルールを探します。



そして、開発者が設定したルールに従ってテストを行い、
このリクエストを許可するかどうかを判断します。


セキュリティールール設定方法

すべてのドキュメントは、データベースのパスと、ドキュメントへの実際のパスの下にあります。


例えば、restaurants/hotdogというドキュメントがあった場合、
そのドキュメントまでのパスは/databases//documents/restaurants/hotdogです。



基本的な読み取り・書き込みルール


以下はデフォルトのセキュリティルールです。



service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
パスのネスト

Firestoreでは階層になったデータを扱うことになります。
その度に、フルパスを書いていては疲れるしエラーの原因にもなるかもしれません。



そこでFirestoreではパスをネストさせることが可能です。


service cloud.firestore {
  match /databases/{database}/documents {
      match /restaurants/{restaurantID}{
      }
      match /restaurants/{restaurantID}/reviews/{reviewID} {
      }  
   }
}

コレクションのみを指定したい場合


service cloud.firestore {
  match /databases/{database}/documents {
      match /restaurants/ {
      // ワイルドカードなしでコレクションのみを指定したこれでも動くが、、
      }  
      match /restaurants/{restaurantID} {
      // こっちを使うほうがいい
      }
   }
}
ワイルドカード

ワイルドカードは2種類あります。


{}で囲むワイルドカードは、
単一のパス要素(例えば、ドキュメントやコレクション)にマッチします。





{=**}で囲むワイルドカードはパスの残りをマッチしてこの変数に格納します。



下の画像でいうと、usersコレクション以下の
サブコレクション・ドキュメント全て指定ということになります。






{=**}を使って「users」コレクション全てにreadを許可した場合、
後から「users」コレクションのサブコレクション「privateData」のドキュメントに
アクセス権を指定しようとしてもきちんと動かないことに注意が必要です。




また、以下の5種類のアクションごとにBoolean値を提供することで
送られてきたリクエストを受け入れるかどうかを設定することもできます。



get→ユーザーが特定のドキュメントを取得するリクエストを送ったとき
list→ユーザーがドキュメントを返すクエリを実行した時
create→新しいドキュメントを作成
delete→ドキュメントを削除
update→ 既に存在するドキュメントを更新



getとlistは一つのアクション、readとして分類されます。
createとupdate, deleteはwriteとして分類されます。



allow read: if true;またはallow read;で誰でもreadができるようになります。






allow write: if false;でwriteを拒否できます。



実際には、ログインしているユーザーならreadを許可する、ユーザーIDが〇〇ならwriteを許可する、
というような条件と一緒に使われることが多いです。
よって、基本的には以下のデータを元に条件を作成していくことになります。



1. Request data



これはリクエストオブジェクトと呼ばれます。
このリクエストオブジェクトには主に以下の2種類があります。





authオブジェクト→Firebase Authなどでログインしたユーザーの情報が含まれている



以下はログイン済みのユーザーなら読み込みを許可する、
というセキュリティルールです。





request.resourceオブジェクト→ユーザーがwriteしようとしている
全てのフィールドのドキュメントの情報が含まれている。
このドキュメントのデータに基づいて動的にリクエストを許可・拒否することができます。



このオブジェクトは、いくつかのメタデータフィールドを持っていますが、
実際のコンテンツのほとんどはrequest.resource.dataプロパティにあります。



例えば、scoreフィールドにアクセスしたい場合、
以下のようにすることでアクセスができます。


request.resource.data.score
request.resource.data[“score”]

Firebaseはスキーマレスですが、セキュリティルールを使用して以下のように、
DBに保存するデータの型や数値に制限をかけることもできます。







2. Resource Object



request.resourceオブジェクトに似ていますが、request.resourceは
「書き込もうとするドキュメントを表す」のに対して、
Resourceオブジェクトはデータベースに「既に存在するドキュメント、
つまり読み込もうとしているドキュメントや更新しようとしているドキュメントを表す」ことです。



他のドキュメント (データベース内の任意のドキュメント) へのアクセス

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




get() 関数→リソースオブジェクトに似たオブジェクトを返す
exist() 関数→特定のものが存在するかどうかをチェックする




どういうときに使うか?




一番よくあるユースケースは、特定のドキュメントにアクセス
できるユーザーのリストを保存したい場合です。
そして、このセキュリティルールを使って、後でそのリストを評価することができます。



例えば、レストランの情報が入ったドキュメントを編集できるのは、
プライベートなドキュメントに「編集者」として登録されている人のみ、
というルールを設定したいとします。



このような感じです。






その方法は以下の通りです。


service cloud.firstore {
 match /database/{database}/documents {
   match /restaurants/{restaurantID} {
    allow update: If get(/database/$(database)/documents/restaurants/$(restaurantID)/private_data/private).data.roles[request.auth.uid] == "editor"
  }

ワイルドカードでレストランIDを指定しています。
get関数がrestaurantIDという変数を使うことを認識できるように、変数に${}を付けなければならないことに注意が必要です。
${}を使わない場合、get関数はレストランIDを文字列だと判断してしまいます。



get関数()を使いget(/database/$(database)/documents/restaurants/$(restaurantID)/private_data/private)
プライベートデータを取得しています。
そして、その取得したプライベートデータのrolesフィールド内で、リクエストで送られてきたユーザーIDがeditorという値を持っているかチェックしています。



「オーナーと編集者」がレストランの情報を編集できる、という設定をしたい場合このように書きます。

roles[request.auth.uid] in ["editor", "owener"]

また、アプリの管理者など全ての情報を編集できるグローバルなアクセス権を持つ人がいる場合、
カスタムクレームを使うといいです。




カスタムクレームは、サーバーサイドライブラリや
クラウドファンクションを使って特定のユーザーに対して設定するカスタム変数です。




カスタムクレームを使うと
このように、管理者ならどこでも書き込みができる、という設定ができます。







カスタムクレームについての公式Doc:
Control Access with Custom Claims and Security Rules  |  Firebase Authentication


カスタム関数

セキュリティルールは、システムが大きくなったり複雑になったりすると、少しごちゃごちゃして冗長になり始めます。
セキュリティルールはこの問題を解決するための良い方法を持っています。



それは、カスタム関数を書くことです。
ほとんどの場合、これらの関数は1つの戻り値のステートメントを持つだけの関数です。
しかし、冗長なコードを避けるためと、セキュリティルールに実際に何をさせたいかを明確にするために、非常に役に立ちます。
例えば、ユーザーが Google のアカウントを持っているかどうかをチェックするカスタム関数を次のように書くことができます。






セキュリティルールは非常に強力で、カスタムルール関数を使用すれば、
かなり高度で複雑なルールセットを追加することができます。


このようなコードを





関数名に役割を持たせ何をするコードなのか一目でみてわかるようにすることができます。





もっと可読性を良くするために、さらに細かくわけることももちろん可能です。







カスタム関数の欠点は?


特に本番環境では、リクエストが拒否された場合、その理由についての説明が一切ありません。
何が原因で拒否されているのかがわからないのはフラストレーションが溜まります。



この問題を解決するために、Firestoreにはルールシミュレータが用意されており、
セキュリティルールに対するあらゆる操作をテストすることができるようになっています。
コンソールは、これらの呼び出しが成功したとき、どのルールがそれを許可したか、
または失敗したとき、どのルールがそれをブロックしたかを教えてくれます。


ルールはフィルターではない



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


セキュリティルールは、各クエリをその結果に対して評価し、
クライアントが読む権限を持っていないドキュメントを返す可能性がある場合、
そのリクエストを失敗させます。クエリは、セキュリティルールで設定された制約に従わなければなりません。



7に続きます。