Firestore rules tips

改訂版を書きました。よければ下記のリンクに飛んでいただいて一読してもらえると嬉しいです！

- [【改訂版】 Firebase Cloud Firestore rules tips](https://tech-blog.sgr-ksmt.org/2018/12/11/194022/)

以下の内容は古い可能性があります。アーカイブとして当時の状態のまま残しておきます



<br />
<br />
<br />
<br />
<br />
<br />

---

この記事は、当初[Firebase.yebisu #2](https://connpass.com/event/75511/)で発表しようと思っていた内容をまとめたものです。
（私がインフルエンザに罹ってしまい発表できなくなったため...)
なんとか当日に間に合わすぞと書いたので、多分また後日更新したりサンプルあげたりします、ご了承ください


---

皆さんはFirestore使ってますか？使ってる人はrulesも書けていますか？
今日は↓みたいな状態から脱却するための基礎知識やtipsをまとめていこうと思います。

```swift:ルール何書いたらいいの🤔
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}
```

## 基本は許可制
基本的に`allow xxx: if yyyy;` で条件を書かなければ、デフォルトでは全てfalseと解釈されます。なので、以下の例だと `/foo`に対するドキュメントの読み書きはOKですが、 `/bar`、`/baz に対するドキュメントの読み書きはNGです。
不用意にallowしなければセキュリティとしては高い状態を保てます。強い。

```swift
service cloud.firestore {
  match /databases/{database}/documents {
    match /foo/{fooID} {
      allow read, write: if true; // fooドキュメントだけ読み書きが許された世界
    }
  }
}
```

また、falseの時を丁寧に書く必要はないです。

ちなみに、

```swift
service cloud.firestore {
  match /databases/{database}/documents {
  }
}
```

ここまではおまじないと思ってもらえれば大丈夫です。 `/databases/{database}/documents` で、FirestoreのDBの `/` (ルート) を指し示していると思ってもらえたら。

## matchはネストして書ける
基本的にはmatch文は直列で長く書くよりはネストさせて書いたほうが絶対良いです。

- 階層が把握しやすい
- 各階層のドキュメントに付与するワイルドカード変数の使用、関数定義のときに都合が良い

のが挙げられます。


```swift:before
service cloud.firestore {
  match /databases/{database}/documents/user/{userID} {
    // ...
  }

  match /databases/{database}/documents/blog/{blogID} {
    // ...
  }

  match /databases/{database}/documents/blog/{blogID}/images/{imageID} {
    // ...
  }
}
```

:thinking: 

```swift:after
service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userID} {
      // ...
    }

    match /blog/{blogID} {
      // ...

      match /images/{imageID} {
        // ...
      }
    }
  }
}
```

:tada: 

## allowできる動作
基本的にはread/writeで、それぞれ更に細かく分類されています。

- read
  - get
  - list (queryでゲットするときとか)
- write
  - create
  - update
  - delete

readはあまりget/listで分類して制御することは少なそう(listで、queryの制限かけることはありそう)だが、writeの場合はdeleteを含んでいるので、
特段deleteを許容しない場合は `allow create, update: if xxx;` としておくと良さそう。
また、それぞれの動作に別々の条件を付けることももちろん可能。

```swift
allow read: if request.auth != null;
allow create, update: if request.auth != null && request.resource.data.userID == request.auth.uid;
allow delete: if request.auth != null && resource.data.userID == request.auth.uid;
```

## ワイルドカード
`match /user/{userID}` の `{userID}`の部分の事を指します。変数名は何でも良いです。私は`xxxID`で統一しています。ちょうど各documentのIDにあたるので。
そのワイルドカードは変数として使うことができ、ルールが実際に適応される時に、参照されるdocumentのIDが内部的に展開され用いられるようになります。
以下は簡単な例です。

```swift
service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userID} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == userID;
    }
  }
}
```

## 基本はカスケードされない
基本的にはルールはサブコレクションに勝手に反映されない（カスケードされない）ので、別途自分でサブコレクションを掘ってルールを書くか、今のルールのままカスケードしてくれと明示的に書く必要があります。
カスケードにするには、match文のpathのワイルドカード変数のあとに `=**` を付与してあげます。

```swift
service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userID=**} {
      // ...
    }
  }
}
```

これで、`/user/{userID}`以下のサブコレクション全てに、`/user/{userID}`と同じルールが適応されます。
ちなみにカスケードを適応した場合、その中にmatch文をネストして書くことができなくなるので注意です。

```swift:この書き方はできなくなる
service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userID=**} {
      // ...
      match /item/{itemID} {
        // ... 
      }
    }
  }
}
```

「Glob(**) resource rules should not have child resource rules.」　というエラーが発生します。

### `allChildren=**`
上述のカスケードでは、そのドキュメントと、そのドキュメント以下の全てのサブコレクションでルールを分けたい場合に困ることになります。
そういう時は以下のように`allChildren=**`を用いると、ルールを分けることができます

```swift:
service cloud.firestore {
  match /databases/{database}/documents {
    match /article/{articleID} {
      allow read: if wwww;
      allow write: if xxxx;
      match {allChildren=**} {
        allow read: if yyyy;
        allow write: if zzzz;
      }
    }
  }
}
```

これで、articleドキュメントに対するルールと、article以下の全てのサブコレクションに対する一律のルールとで分離することができます。

## ルールは狭く深く、不用意に浅いところで許可しない
Firestoreで重要なルールとして、許可制であることと、もう一つはルールはなるべく狭く深いところで必要なときに許可してあげることです。
あるドキュメントに対して、複数のルールが別々に定義されていた場合は、それらのルールが `and` ではなく、 `or` で評価される点に注意してください。
例えば、 `/user/{userID}`に対して

- `allow write: if request.auth != null && request.auth.uid == userID`
- ***`allow write: if true`***

という複数条件が与えられて（しまっていた）場合、前者で防げていたとしても、後者が成立してしまい、結果として書き込みが許可されてしまうので、ルールの意味を成さなくなります。

### よく嵌まる例
各種モデルに一律で`request.auth != null`であることをread, writeに付与しようと思って次のように書きます。

```swift
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }

    function isUserAuthenticated(userID) {
      return request.auth.uid == userID;
    }

    // `/` 以下に isAuthenticated() を適応
    match /{allChildren=**} {
      allow read, write: if isAuthenticated();
    }

    match /user/{userID} {
      // userIDとauth.uidが一致するか且つ、何かしらの条件に一致する
      allow read, create, update: if isUserAuthenticated(userID) && someCondition();
    }
  }
}
```

実はこれは間違いで、これも`/user/{userID}`に対して、認証さえ通っているユーザーであれば自由に書き込みができてしまう状態になってしまいます。穴だらけ。。


正しくは以下のように面倒でも狭く深くするのが正解です。

```swift
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }

    function isUserAuthenticated(userID) {
      return request.auth.uid == userID;
    }

    match /user/{userID} {
      allow read, create, update: if isAuthenticated() 
                                  && isUserAuthenticated(userID) 
                                  && someCondition;
    }
  }
}
```

## 関数定義を活用する
Realtime Databaseのルールとの大きな違いの1つとして、 **自分で関数が定義できる点** が挙げられます。
定義の形としてはこんな感じです。引数、返戻値は自由に決められます。

```swift
function foobar(arg) {
}
```

また、定義箇所も自由で、状況に応じて配置することが出来ます。
自分のプロジェクトでは、以下のような関数を定義して、何度も何度も同じような処理を書くのを避けるようにしています。


```swift
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return request.auth != null;
    }

    function isUserAuthenticated(userID) {
      return request.auth.uid == userID;
    }

    match /version/{v} {
      function getUser(userID) {
        return get(/databases/$(database)/documents/version/$(v)/user/$(userID));
      }

      match /user/{userID} {
        allow read: if isAuthenticated();
        allow write: if isUserAuthenticated(userID);
      }
      
      match /article/{articleID} {
        allow read: if true;
        allow write: if isUserAuthenticated(request.resource.data.userID) 
                     && getUser(request.resource.data.userID).data.isActive == true
      }
    }
  }
}
```

ちなみに`getUser`関数だけ違う場所に書いているのは、get関数でpathを指定する時に、versionまでのワイルドカード変数が必要だからです。

## 事前に組み込まれている関数、変数を活用する
事前に準備されているものが多いので、実はルールを構成するのにそんなに困らなかったりします。
特に使うなってのをいくつか挙げておきます。

### `request`, `resource`
恐らく最も使う変数。前者は動作実行時の状態を表していて、後者はその時のDBの状態を表しています。
なので`request.resource` と `resource`　では若干異なってきます。
writeの権限で、 `request.resource.data.xxx == resource.data.xxx` として値が変わっていないかの判定で書いたりもします。
尚delete実行の場合は`request.resource.data`に値が入ってこないことがあるので、deleteの時にfield valueを参照したいときは`resource.data`を見るのが良さそう。

### `request.auth`
auth認証が通っている前提で組んでいくのであればほぼ使うと思います。また、user的なモデルのdocumentID==auth.uidとして作成するようにしていると色々やりやすいです。

### `get()`、`exists()`
事前に準備されている関数で、引数にpathを渡してあげると、前者は存在すればそのオブジェクトを返し、後者は存在しているかどうかをbool値で返してくれる

```swift
get(/databases/$(database)/documents/user/$(userID))
```

pathは`/database`から始める必要がある。
`$(変数)`で変数展開が行える。
返ってくるオブジェクトのfield valueを取りたい場合は、`get(...).data.name`のように書く。

### `in`

`xxx in yyy` のようにして、 xxxがyyy(のmap)に含まれているかみたいなのをチェックするのに使う

```swift
match /group/{groupID} {
  allow read: if request.auth.uid in request.resource.data.members
}
```

詳しくは[ここ](https://firebase.google.com/docs/firestore/reference/security/?hl=ja)見るとどんなのがあるのか分かります。

## 更新時に特定のkeyの値が変更されていないことを保証したい
例えばcreate時に入れた情報のうち、`name`フィールドだけは、その後のupdateで書き換え不可にしたい場合なんかは以下のようにする。

```swift
match /user/{userID} {
  allow read: if ...;
  allow create: if ...;
  allow update: if request.auth.uid != userID && request.resource.data.name == resource.data.name;
}
```

これで、更新はユーザー本人かつ、nameフィールドは作成時のものから変更されていない時のみ許可となる。複数あれば `&&` で繋げていく。

## Field ValueがObjectの場合の、Object内の値を参照したい
FirestoreではField valueにObject(json)を持つことが出来ます

<img width="190" alt="スクリーンショット 2018-02-20 19.05.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/27802/19724a95-29e1-11e4-ff32-c598b5e684e9.png">

こういうやつですね。

この、例で言うfoo,barにrule上でアクセスしたい場合ですが、以下のようにしてアクセスすることができます。

```swift
request.resource.data.info.foo
request.resource.data.info.foo
```

特に躓くポイントはなく。ただ、Objectでの `==` は（恐らく)うまく動かない。
ので中身までチェックしてあげるのが吉。


## Field ValueがReferenceの場合のあれこれ
また、Firestoreは他のdocumentへの参照をfield valueとして持つことができます。

<img width="488" alt="スクリーンショット 2018-02-20 19.09.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/27802/42fa2cf6-2efa-4485-ad6e-e79559c87ef4.png">

こういうやつですね。

rule上では、path型のオブジェクトとして扱われるので、以下のような利用が可能です。

```swift
// path型オブジェクトなのでそのままget(),exists()に渡せる
get(request.resource.data.userReference)
exists(request.resource.data.userReference)
request.resource.data.userReference is path // true
```

また、特定のreferenceと、IDを渡してあげて、参照先が一致するか判定する関数もこんな感じで組めます。

```swift
function matchUserReference(ref, userID) {
  return ref != null && ref == get(/databases/$(database)/documents/version/$(v)/user/$(userID)).__name__;
}
```

`__name__`は事前に用意された変数で、オブジェクトのpathを返してくれます。

以下は雑多な例ですが、記事を作成、更新する時に、article.userReferenceをセットするのが必須で、
さらにその参照が`request.auth.uid`と一致するuserの参照と一致していたら許可、みたいな感じになっています。

```swift
match /article/{articleID} {
  allow read: if true;
  allow write: if matchUserReference(request.resource.data.userReference, request.auth.uid)
}
```


## サンプルないの？
今週中にはなんとかあげたい気持ちです、もうしばらくおまちください... :pray: 


## ruleはgit管理+CLIでデプロイにする
毎回webのコンソールで書いて更新..だと非効率だし変更管理もできないので、`firestore.rules`ファイルをgit管理し、CLIでデプロイするようにします。

### セットアップ
まだ手元でセットアップしていなければ

```bash
$ firebase init firestore
```

を行い、指示に従ってセットアップすると `firestore.rules`が生成されます。（内容は直近のconsoleの内容が入ってくるはず)

### デプロイ
ルール**のみ**デプロイする場合は

```bash
$ firebase deploy --only firestore:rules
```

でいけます。

## ルール書くのにおすすめのプラグイン
VSCodeを使っているなら[toba/vsfire](https://marketplace.visualstudio.com/items?itemName=toba.vsfire)がおすすめです。
rulesを書く時に　syntaxが効いてくれるので書きやすくなります。ある程度の予約語(?)ならauto-completionもかかります。

![](https://github.com/toba/vsfire/raw/master/images/rules-completions.gif)

それ以外は、、、 :thinking: 

## まとめ
駆け足になりましたが以上になります。ぼちぼち更新します。
firebase.yebisu#2で発表できなかったのが悔しい...

## 参考
- [Cloud Firestore セキュリティ ルールを設定する](https://firebase.google.com/docs/firestore/security/get-started?hl=ja)
- [
Cloud Firestore セキュリティ ルールをカスタマイズする](https://firebase.google.com/docs/firestore/security/secure-data?hl=ja)
- [
Cloud Firestore Security Rules Reference](https://firebase.google.com/docs/firestore/reference/security/?hl=ja)