【get()は駄目】Firestoreで複数検索時に所属グループ判定したいのにできない。

この記事は約6分で読めます。

はじめに

Firestore のセキュリティルールでは、ルールの中で get()exists() を使用して他のドキュメントを参照した際に、評価できないと判断されると自動的に「権限なし(permission denied)」として扱われます。

つまり、セキュリティルール内での評価中にエラーが発生すると、そのリクエストは無条件で拒否されるという仕様です。

そのため、意図せず get() の多用によって評価が失敗すると、実際にアクセス可能な条件であっても「アクセス権がない」とみなされてしまいます。このためにFirestoreでクエリを実行した際に「no efficient permission」エラーが発生してハマりました。

問題のコード

❌ 問題のあるセキュリティルール

Bash
match /children/{childId}{
  allow read :if isAuthenticated() &&       // ここではget()を使用していない
    isGroupMember(getChildIdGroup(scoutId));
}

function getChildIdGroup(childId){
  return get(/databases/$(database)/documents/children/$(childId)).data.belongs
}

// GroupIdで渡されたグループのMember(trueならview↑が確定する)
function isGroupMember(groupId){
  return exists(/databases/$(database)/documents/groups/$(groupId)/members/$(getUserUID()))
}

なぜエラーが発生するのか

get()関数,exists()の問題点

  • セキュリティルール内でget()を使うと別のドキュメントを読み取る事ができる。ここでは、要求されたchildレコードが所属するgroupに利用者も認証されているかを確認するのに使用。
  • 1つだけ取得するような場合であればこれでも問題ない
  • queryを書いて複数取得するような時、この参照処理が検索結果でヒットした数だけ、それぞれ実行される。
  • 外部ドキュメントの参照の回数が多すぎるとエラー扱いで「no efficient permisisons」となる。
  • このせいで、getは通るのにlistが通らないという謎現象が起きる。

resource / get() / exists() の使い分けと違い

FirestoreのRuleに書いた処理は、操作対象のドキュメントを読み込んだ段階でスタートするということを忘れてはいけない。

resource

  • 対象ドキュメント(ルールの match にマッチしたドキュメント)のデータにアクセスできる変数
  • 既にFirestoreが取得済みの情報なので追加の読み取りコストが不要
  • クエリに対しても効率的に動作し、list や read のルールで推奨される
Bash
allow read: if resource.data.groupId == request.auth.uid;

get(path)

  • 明示的に別のドキュメントを読み取って権限判定を行う関数
  • 毎回読み取りが発生する。
  • 個別アクセスに使う分には問題ないが、list や where 句を使うクエリと併用するのはいけない。
Bash
function isInGroup(groupId) {
  return get(/databases/$(database)/documents/groups/$(groupId)).data.members.hasAny([request.auth.uid]);
}

exists(path)

  • 対象ドキュメントが存在するかどうかを真偽値で返す関数
  • get()と同様に内部的には読み取りが発生する
Bash
function isProfileCreated(uid) {
  return exists(/databases/$(database)/documents/users/$(uid));
}

使い分けの原則

  • なるべく resource を使う:効率よく権限チェックできる
  • get() はピンポイントアクセス用途にとどめる:大量データとの併用は避ける

関数の返り値がキャッシュされる場合

get()を1000回実行できる場合があります。ドキュメントには以下のように記述されています。

アクセス呼び出しの制限

ルールセット評価ごとのドキュメントのアクセス呼び出しには制限があります。

  • 単一ドキュメントに対するリクエストとクエリ リクエストの場合は 10。
  • 複数のドキュメントに対する読み取り、トランザクション、バッチ書き込みの場合は 20。各オペレーションには、上記の制限の 10 も適用されます。(以下略)

いずれかの上限を超えると、アクセス拒否のエラーが発生します。

一部のドキュメントに対するアクセス呼び出しはキャッシュされる場合があります。キャッシュされた呼び出しは制限数に計上されません。

ここで気になるのはキャッシュされる条件ですが、公式見解はどこにもありません

私の感想によると、おそらく関数に渡すドキュメントのパスが完全に一致していればキャッシュされると思います。しらんけど

まとめ

  • get()を排除: 別ドキュメントの読み取りをやめる
  • resource.dataを使用: 対象ドキュメント自体のデータで権限チェック
  • クエリ条件との整合性: where(“personal.belongs”, “==”, groupId)との組み合わせ

コメント

タイトルとURLをコピーしました