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