firestoreのSecurity Ruleを設定して特定のuserのみが投稿を閲覧できるようにする

この記事は[Firebase #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/firebase-2)の記事です。

# はじめに
某webマーケティング会社でフロントエンドエンジニアとして働きながら、バンド活動もおこなっているtarooooooooooです。
バンド内での貸し借りを可視化するために「Cashica」というサービスを作っています。
詳細についてはこちらの記事からどうぞ[nuxt.js + firebase でPWA,SSRな借りパクを撲滅するアプリを作っている話](https://qiita.com/taroodr/items/bc7f3a14abd474a55985)

本記事では、上記サービスを作る際に学んだfirestore設計の知見を記載します。

# 設計
postsはドキュメントのIDはfirestoreによる自動割り当て、コレクションには貸し借りしているモノの詳細と、貸しているuser、借りているuserへの参照が入っています。

usersはドキュメントのIDにOauth認証の際に取得したuidを使っています。
ここでポイントとなるのが、usersのドキュメントIDを自動割り当てではなく、uidを用いているところになります。
こうしておくことで後々行うSecurity Ruleの設定が楽になります。

![easypay – Database – Firebase console.png](https://qiita-image-store.s3.amazonaws.com/0/201963/c177ce44-a037-3cab-d37e-59946bb1d152.png)
![easypay – Database – Firebase console (1).png](https://qiita-image-store.s3.amazonaws.com/0/201963/a9a139ed-9446-f551-60e8-aa5345578c6a.png)

# データの追加
データを追加するコードを見ていきましょう。（一部抜粋です）
## userの追加
userには`firebase.auth().onAuthStateChanged()`というメソッドで取得した認証情報が入っています。

```javascript

db.collection('users').doc(user.uid).set({
  uid: user.uid,
  name: user.displayName,
  email: user.email,
  icon: user.photoURL
})
```

## postの追加
from,toはそれぞれ貸すユーザ、借りているユーザのuidが入っています。
`db.collection('users').doc(from)`のようにフィールドの値を設定すると参照型で保存がされます。

```javascript
db.collection('posts').add({
  from: db.collection('users').doc(from),
  to: db.collection('users').doc(to),
  description,
  deadline,
  createTime: firebase.firestore.FieldValue.serverTimestamp()
})
```

## Security Ruleの設定
下記のテンプレートに追加する形でセキュリティルールを記載していきます。

- postsのドキュメントはフィールド内のto,fromに参照型で保存されているユーザのみが読み取り・更新・削除できる
- postsの投稿は認証されているユーザであればできる
- user情報の読み取りは認証されていればできる
- user情報の書き換えはuser本人のみができる

```javascript
service cloud.firestore {
  match /databases/{database}/documents {
 }
}
```
### postsのドキュメントはフィールド内のto,fromに参照型で保存されているユーザのみが読み取り・更新・削除できる
- `request.auth.uid`は認証が通っているuserのID、
- `resource.data.to`は読み書きしたいドキュメントのtoフィールドの値

usersのドキュメントIDを自動割り当てではなく、uidを用いているため以下のように、読み取りしたいドキュメントのto,fromの参照型の値と認証が通っているユーザへのpathとの比較が可能となります

```javascript
service cloud.firestore {
  match /databases/{database}/documents {
    match /posts/{postId} {
      allow read, update, delete: if resource.data.to == /databases/$(database)/documents/users/$(request.auth.uid) ||
        resource.data.from == /databases/$(database)/documents/users/$(request.auth.uid);
    }
 }
}
```

### postsの投稿は認証されているユーザであればできる
`isAuthenticated()`は認証されているかどうかを判断してくれるので下記のように追記します。

```javascript
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }
    match /posts/{postId} {
      allow read, update, delete: if resource.data.to == /databases/$(database)/documents/users/$(request.auth.uid) ||
        resource.data.from == /databases/$(database)/documents/users/$(request.auth.uid);
      allow create: if isAuthenticated();
    }
 }
}
```

### user情報の読み取りと、作成は認証されていればできる
`allow get, create: if isAuthenticated();`とすることで可能になります。

```javascript
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }
    match /posts/{postId} {
      allow read, update, delete: if resource.data.to == /databases/$(database)/documents/users/$(request.auth.uid) ||
        resource.data.from == /databases/$(database)/documents/users/$(request.auth.uid);
      allow create: if isAuthenticated();
    }
    match /users/{userId} {
      allow get, create: if isAuthenticated();
    }
  }
}
```

### user情報の書き換えはuser本人のみができる
`request.auth.uid == resource.data.uid`で認証が通っているユーザと対象ドキュメントのuidとの比較ができ、同じ場合のみ許可します。

```javascript
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }
    function isUserAuthenticated(userId) {
      return request.auth.uid == userId
    }
    match /posts/{postId} {
      allow read, update, delete: if resource.data.to == /databases/$(database)/documents/users/$(request.auth.uid) ||
        resource.data.from == /databases/$(database)/documents/users/$(request.auth.uid);
      allow create: if isAuthenticated();
    }
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow update, delete: if request.auth.uid == resource.data.uid;
    }
  }
}
```

# おわりに
初めはセキュリティルールの設定のことを想定していなかったので、ルールを設定する段階でデータ構造の見直しが必要になってしまったので、初めからある程度想定しておいた方が幸せな気がしました。

サービスのDEMOとGitHubリポジトリは下記です。

GitHub: https://github.com/taroodr/cashica
Demo: https://easypay-dd04a.firebaseapp.com

# 参考
https://medium.com/google-cloud-jp/firestore2-920ac799345c
https://tech-blog.sgr-ksmt.org/2018/12/11/194022/
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja
https://firebase.google.com/docs/firestore/security/get-started?hl=ja