実例で考えるfirestoreのセキュリティパターン

firestoreのセキュリテルールをきちんと行うために勉強したので、成果物をまとめて設定実例をご紹介します。

# firestoreの基本

初期状態では、以下のようになっています

```js:firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}
```

この状態だと条件を消さずに

```js:firestore.rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
      match /hoge/{hogeId}　{
        read,write: request.auth.uid == hogeId;
      }
    }
  }
}
```

のように設定したくなりますが、実は、firestoreのセキュリティルールは、wildcardを用いたルール設定ではorによるfilter形式なので、<font color="DeepSkyBlue">一度でも、親の条件がtrueになると、それ以降のネスト先の条件がfalseであってもアクセス許可を与えてしまうので、注意が必要です</font>。つまり、wildcardを用いてそれ以降のドキュメントに対して同じ条件を設定することもできますが、ゆるい制限を設けた後に徐々に深い制限をかけていくということができないので基本的にルールを共通化せずにサブコレクションでも個別にルールを設定する方が面倒ごとを避けることができます。

この辺りの基礎事項に関しては、良記事がたくさんあります。参考にしたのは以下の資料。

[Google公式ドキュメント](https://firebase.google.com/docs/firestore/security/rules-conditions?hl=ja)
[Qiita - Firestore rules tips](https://qiita.com/sgr-ksmt/items/1a731fdadf06119d35fc)
[Cloud Firestore セキュリティ ルールをカスタマイズする](https://firebase.google.com/docs/firestore/security/secure-data?hl=ja)
[Cloud Firestoreの勘所 パート3 — セキュリティルール](https://medium.com/google-cloud-jp/firestore3-9518331f8748)
[Controlling Data Access Using Firebase Auth Custom Claims (Firecasts)](https://www.youtube.com/watch?v=3hj_r_N0qMs#action=share)

特に英語が得意な方は動画が疑問に隅々に答えてくれていてわかりやすかったので、動画で学ぶことをおすすめします。(それにしても再生数が少なすぎる気が。。。)

#　主なルールはカスタムファンクションにしておく

現場では、以下のようにおきまりの処理はカスタムファンクション化して全体適用が可能な状態にしています。
以下のadminControlはCustomClaimを使っています。

```ts
 function adminControl() {
  return request.auth.token.admin == true;
 }
 function isLogin() {
   return request.auth.uid != null;
 }
 function onlyIdentityUser(userId) {
   return request.auth.uid == userId;
 }
```

## CustomClaimについて

Custom ClaimはFirebase Authを使用して取得できるユーザー認証情報にメタデータを付随させることができる機能です。これによって`Firebase.auth().getUser`によって取得されるユーザーデータに任意のデータ構造を追加することによって、セキュリティルールの評価時にこのメタデータの値をチェックすることができます。以下の例では、adminという属性をつけて、この値を持っているユーザーにfirestore上の任意のコレクション接続許可を与えています。

```config.ts
import * as admin from 'firebase-admin';
import * as serviceAccount from 'firebase.service-account.json';

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount as admin.ServiceAccount)
});

admin.firestore().settings({
  timestampsInSnapshots: true
});

export { admin };
```

```setCustomClaime.ts
import { admin } from './config';

export async function setAdminRole (userId) {
  return admin.auth().setCustomUserClaims(userId, { admin: true });
}

export async function checkAdminRole (userId) {
  return admin.auth().getUser(userId);
}

export async function resetRoll (userId) {
  return admin.auth().setCustomUserClaims(userId, null);
}
```

以上のsetAdminRoleによって認証済みのfirebaseユーザーに対して、個別にadmin権限をつけることによって、上述の

```ts
function adminControl() {
  return request.auth.token.admin == true;
 }
```

セキュリティルールによってtokenプロパティから取得できるカスタムクレームによって条件を記述することができます

# matchのスコープを意識する

サブコレクションは評価のタイミングではサブコレクションのドキュメントのスコープを指すため、コレクションの参照元ドキュメントを取得するためにresouceは使うことができません。この制限によって、サブコレクションのセキュリティルールが取得元のドキュメントのパラメータに遺存する場合は、必ず、getやexistを用いる必要があります。

```js:firestore.rules

match /parent/{parentId} {
  function checkPermissionParentUser() {
    return (request.auth.uid in resource.data.members);
  }
  allow read: if checkPermissionParentUser();
  allow write: if checkPermissionParentUser();
  match /childen/{childId} {
    function checkPermissionChildUser(parentId) {
      return (request.auth.uid in get(/databases/$(database)/documents/parents/$(parentId)).data.members);
    }
    allow read: if checkPermissionChildUser(parentId);
    allow create: if checkPermissionChildUser(parentId);
    allow update: if checkPermissionChildUser(parentId);
    allow delete: if adminControl();
  }
}
```

以上の例では、checkPermissionParentUserはresourceから取得しているため、get,existによる課金を抑えることができるので分けて設定しています。

### 注意点

1.`get`を使用する場合、必ず、<font color="DeepSkyBlue">絶対パス</font>でデータを指定する必要があります。つまり`/databases/$(database)/documents/`を省略することはできません。（筆者はこれをmatchからの相対パスで設定しようとしていたためハマりました。）

2.`{valiable}`の書式のvariableの部分はリクエスト時に取得先ドキュメントのIDが取得されます。これは、matchでネストしたスコープ全てで有効ですが、ネスト外のコレクションリクエスト時には取得できません。つまり、リクエストしたコレクションやドキュメントに対して、セキュリティルールの設定に必要な情報が全て取得先のデータに紐づいているか、request.auth.idから連鎖的に取得可能であることを確認する必要があります。

3. {}はキャプチャを表し、$()が変数取得を表します。またオブジェクトのkeyの変数指定は[(key)]で指定するようで、この記法でないとsyntax colorが効かなかったのと[key]で指定しているドキュメントはなかったので、やらない方がいいかと思います(動作は未検証です、AngularのbindModelっぽいparserでも使ってるんでしょうか。。。)。