Firestore セキュリティールールはしっかり設定しよう（戒め）

# はじめに
皆さん、firestoreしていますか!?（語彙力）
firestoreを使うとバックエンドの構築なしでサクサク開発できて楽しいですよね！
しかし気をつけないことが一つあります。それはセキュリティールールの設定です。

firestoreを用いて開発する際には基本的にはSDKを用いてデータの読み取りや書き込みを行っていくと思います。
しかし、firestoreではSDK以外にもデータを操作する方法を提供しており、Cloud Firestore API[^1]を用いてでもデータの読み書きをすることができます。
そして、適切なセキュリティールールが設定されていないと、このCloud Firestore APIを使って簡単に意図せぬデータの読み書きが行えてしまうのです...!
それでは適切なセキュリティールールの設定がされていなかった場合どのようなことができてしまうのかを段階的に見ていきましょう！

# スキーマ編
firestoreで新規プロジェクトを作成した場合本番モードとテストモードを最初に選ぶことができますが、大抵の方はテストモードを選択されると思います。
この時点では以下のように１ヶ月間読み書きができる様になっています。
![スクリーンショット 2020-12-16 11.55.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/936527/80b73312-ca2f-fd0c-629f-5a40b7fc9441.png)
例えば以下のように画面を作成し、
![スクリーンショット 2020-12-16 12.01.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/936527/84a987ae-874f-d161-6dd9-0135860ff61b.png)
対応するコレクションのスキーマは以下のようなものを想定します。

```
todos: [
　todo: { description: '白菜を買う'},
　todo: { description: 'firebaseを完全に理解する'},
　...
]
```
画面側でADDボタンをクリックするとtodosコレクションに`{description: 'hoge'}`のような形で、
インプットの値をdescriptionフィールドの値として保存します。
画面上では適切にSDKを用いて実装、バリデーションなどを行っていれば予期せぬデータの操作はできなくなっています。しかし前述のCloud Firestore APIを使えば画面の外側からでも直接データの操作ができます。
例えばドキュメントの作成であれば
`curl -X POST -H "Content-Type: application/json" -d '{"fields": {フィールド名: {型: 値}}}' "https://firestore.googleapis.com/v1/{parent=projects/*/databases/*/documents/**}/{collectionId}"`
というコマンドをターミナルから実行することで行うことができます。
Webのプロジェクトでfirebaseが用いられているのであればChromeの開発者ツールのNetworkタブからプロジェクトIDやドキュメントIDは簡単に見ることができます。
例のTODOアプリの画面上でADDボタンをクリックすると以下のようにプロジェクトID（todo-b181f）を取得することができます。
![スクリーンショット 2020-12-16 12.58.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/936527/b0a86a9a-e6df-3ded-6040-e3df9d6b1c9a.png)

以下のコマンドを実行すると
`curl -X POST -H "Content-Type: application/json" -d '{"fields": {"任意の": {"stringValue": "フィールド作成"}}}' 'https://firestore.googleapis.com/v1/projects/todo-b181f/databases/(default)/documents/todos'
`
![スクリーンショット 2020-12-16 13.33.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/936527/047aa14e-09a4-9e10-c292-3dfa4b7f3d61.png)
このようなドキュメントが作成されてしまいます！🙀
description以外のフィールドを作成できないようにするには以下のようなルールを設定します。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todoId} {
      allow create: if
            request.resource.data.size() == 1 &&
            'description' in request.resource.data &&
            request.resource.data.description is string;       
    }
  }
}
```
このままだとcreateしかできないので以下のように変更しread, update, deleteもできるようにしましょう。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todoId} {
      allow create: if
            request.resource.data.size() == 1 &&
            'description' in request.resource.data &&
            request.resource.data.description is string;
      allow read: if true;
      allow update: if
            request.resource.data.size() == 1 &&
            'description' in request.resource.data&&
            request.resource.data.description is string
      allow delete: if true;      
    }
  }
}
```

何かに気付きませんでしたか...?
そうですね

```
request.resource.data.size() == 1 &&
'description' in request.resource.data &&
request.resource.data.description is string;
```
この部分が２回書かれていますね。
セキュリティールールでは関数を作り、同じ処理をまとめておくことができます。

```
function isValidTodo(todo) {
  return todo.size() == 1 &&
         'description' in todo &&
         todo.description is string;
      }
```
このような関数を作っておけば再利用することができ、かつ可読性も上がります。
変更後のセキュリティールールは以下のようになります。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todoId} {
    	function isValidTodo(todo) {
      	  return todo.size() == 1 &&
                 'description' in todo &&
                 todo.description is string;
        }
        allow create: if
      	      isValidTodo(request.resource.data);
        allow read: if true;
        allow update: if
      	      isValidTodo(request.resource.data);
        allow delete: if true;      
    }
  }
}
```
このセキュリティールールの設定で先程のコマンドを実行するとエラーが返ってくるようになり、勝手な値を設定できなくなっています！

```
{
  "error": {
    "code": 403,
    "message": "Missing or insufficient permissions.",
    "status": "PERMISSION_DENIED"
  }
}
```
# 認証編
ログイン機能のあるサービスでは認証されているユーザのみがデータの読み書きを行えるようにするケースがよくあると思います。
Firebaseのセキュリティールールでは認証されているかの判定を行うことができるので、それを用いれば認証状態によるデータの読み書きの制限を行うことができます。
例えば、TODOアプリでread以外のアクションは認証を必須にしたいとします。
一番単純な設定は以下のようになるでしょう。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todoId} {
    	function isValidTodo(todo) {
      	  return todo.size() == 1 &&
                 'description' in todo &&
                 todo.description is string &&
        }
        allow create: if
      	      isValidTodo(request.resource.data) &&
              request.auth.uid != null;
        allow read: if true;
        allow update: if
      	      isValidTodo(request.resource.data);
              request.auth.uid != null;
        allow delete: if 
              request.auth.uid != null;      
    }
  }
}
```
これで一安心...と思いきやそうは行きません。
このままだとログインしているユーザーであれば誰のデータでも削除できるようになってしまいます！
例の如く認証情報もChromeの開発者ツールのNetworkタブから取得し、
![jwt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/936527/aa957cdf-f45c-8fce-6ffe-9f5279cd9983.png)
コマンドを実行することで直接削除できてしまいます。
`curl -X DELETE 'https://firestore.googleapis.com/v1/projects/todo-b181f/databases/(default)/documents/todos/ドキュメントID' -H 'Authorization: Bearer eyJh...' `
todoにuidフィールドをもたせ、作成、削除、更新を行ったrequest.auth.uidと比較してあげることで、自分以外のユーザのデータを操作することを防ぐことができます。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth.uid != null;
    }
    function isOwner(id) {
      return request.auth.uid == id;
    }
    match /todos/{todoId} {
    　function isValidTodo(todo, authId) {
        return todo.size() == 2 &&
               'description' in todo &&
               todo.description is string &&
               'uid' in todo &&
               todo.uid is string &&
               isOwner(todo.uid);
      }
      allow create: if  			
      	    isValidTodo(request.resource.data, request.auth.uid) &&
            isAuthenticated();         
      allow read: if true;
      allow update: if
      	    isValidTodo(request.resource.data, request.auth.uid) &&
            isAuthenticated() &&
            isOwner(resource.data.uid);
      allow delete: if    
            isAuthenticated() &&
            isOwner(resource.data.uid);
    }
  }
}
```
# おわりに
firestoreの特性上、適切なセキュリティールールの設定をしないと好き勝手にデータが書き換えられてしまうことがおわかりいただけたと思います😨
自分自身勉強中なのでこうしたらいいよなどのご意見ありましたらぜひお願いします！











[^1]: https://firebase.google.com/docs/firestore/reference/rest