Cloud Firestoreのセキュリティルールファイルを分割する
September 18, 2021
Firebase CLI で生成された Firebase プロジェクト内の firestore.rules
がセキュリティルールを定義するファイルです。
初期状態の内容は以下です。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
セキュリティルールはすべてこのファイルに書いていくことになりますが、そんなことをしているとこのファイルの内容が膨大な量になってしまい、いつの日か嫌気が差すであろうことは想像に難くありませんよね。
こういうときはファイルを分割しましょう!…という発想に誰もが至りますが、ところがどっこい、これは JavaScript ファイルではありません。
拡張子を見ると、これは .rules
というファイルで、Firestore 固有の書き方になります。いわゆる DSL ですね。
ということで残念ながら import の仕組みは存在しません。ファイル分割の仕組みを期待する声はかなり大きいと思うのですが、公式が提供していないのはなぜなのでしょうね。実際に同じ思いを抱えている人はそれなりに見つかります。
ここで、上記の質問内に出てくる方でセキュリティルールのファイル分割/統合を行うパッケージを作った方がいました。
これらを見て思いつきました。
セキュリティルール結合シェルスクリプトを作ればいいじゃない。
結局のところやりたいことは、複数のファイルを firestore.rules
という1つにまとめるだけですから、それぐらいの処理ならばシェルスクリプトで十分だろうと。
ということで、同じ悩みを抱えている方に向けてスクリプトをご紹介するわけですが、最初に言い訳させてください。自分さえ使えれば良いと作ったものなのでコードが汚いというのと、そもそもシェルスクリプトを書いたのは今回が2、3回目のド素人なので、何がきれいなシェルスクリプトなのかさえ良くわかっていないと言う点です。
でもちゃんと動くことは保証するのでご安心を。シェルスクリプト得意な方はお好きにカスタマイズしてお使いください。
TL;DR #
とりあえず各ファイルを載せます。解説は後述します。
ファイルの配置 #
├ concat
└ rules/
├ header.rules
├ footer.rules
├ routes.rules
├ helper.rules
└ functions/
├ xxx.rules
├ yyy.rules
└ zzz.rules
concat #
#!/bin/bash
readonly rules_dir='/rules'
readonly header_file='header.rules'
readonly footer_file='footer.rules'
readonly route_file='routes.rules'
readonly helper_file='helper.rules'
readonly funcs_dir='/functions'
readonly in_files=(
'xxx.rules'
'yyy.rules'
'zzz.rules'
)
readonly out_file='firestore.rules'
print_source_files() {
echo $route_file
for file in ${in_files[@]}; do
echo $file
done
echo $helper_file
}
main() {
readonly work_file='firestore.rules.work'
touch $work_file
cat ".${rules_dir}/${header_file}" >> $work_file
cat ".${rules_dir}/${route_file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
echo >> $work_file
for file in ${in_files[@]}; do
echo " // ${file} //" >> $work_file
cat ".${rules_dir}${funcs_dir}/${file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
echo >> $work_file
done
echo " // ${helper_file} //" >> $work_file
cat ".${rules_dir}/${helper_file}" | sed -e '/^$/d' | sed 's/^/ /g' >> $work_file
cat ".${rules_dir}/${footer_file}" >> $work_file
cat $work_file > $out_file
rm $work_file
}
print_source_files
main
header.rules #
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
footer.rules #
}
}
routes.rules #
match /xxx/{xxxId} {
allow get: if getXxx();
}
match /yyy/{yyyId} {
allow get: if getYyy(yyyId);
match /zzz/{zzzId} {
allow get: if getZzz(yyyId, zzzId);
}
}
healper.rules #
// @return {boolean}
function isAuthenticated() {
return request.auth != null;
}
// @param {string} id - User id
// @return {boolean}
function uidMatches(id) {
return id == request.auth.uid;
}
// @param {array} paths - e.g. ['collection name', 'document id']
// @return {boolean}
function docExists(paths) {
return exists(getPath(paths));
}
// @return {string} - Authentication UID
function getAuthUid() {
return request.auth.uid;
}
// @return {string} - Authentication Email
function getAuthEmail() {
return request.auth.token.email;
}
// @return {object} - Requested Data
function getRequestedData() {
return request.resource.data;
}
// @return {object} - Resource Data
function getResourceData() {
return resource.data;
}
// @param {array} paths - e.g. ['collection name', 'document id', 'collection name', 'document id']
// @return {path} - Document path
function getPath(paths) {
return path([['databases', database, 'documents'].join('/'), paths.join('/')].join('/'));
}
xxx.rules #
function getXxx() {
return isAuthenticated();
}
yyy.rules #
function getYyy(yyyId) {
return uidMatches(yyyId);
}
zzz.rules #
function getZzz(yyyId, zzzId) {
let authUid = getAuthUid();
let reqData = getRequestedData();
return ~ && ~ && ~; // 実際には ~ に何か条件を書いていく
}
解説 #
concat
が結合シェルスクリプト、それ以外が firestore.rules
を作るための素材(=分割されたファイル)です。
concat
内の print_source_files()
が concat 対象のファイルをコンソールに吐くだけの確認用関数で main()
の方が実際の結合処理を行う関数です。
それ以外のファイルに説明は以下の通りです。
header.rules
: ヘッダー部分の外枠ファイルfooter.rules
: フッター部分の外枠ファイルroutes.rules
: ドキュメントのパスを指定するファイルhelper.rules
: ヘルパー関数を定義しているファイル(これがあると便利です)functions/
配下の各ファイルが実際にtrue
またはfalse
を返す関数で、routes.rules
の1つ1つに対応しています。(xxx.rules
、yyy.rules
、zzz.rules
の記述内容は適当です。)
後は作成した concat
を実行すれば firestore.rules
が生成されます。
./concat
Permission denied
となり実行できなかった場合は、ファイルの権限を変更してください。
chmod 755 concat
結果 #
上記内容で concat
を動かすと、以下 firestore.rules
が作成されました。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /xxx/{xxxId} {
allow get: if getXxx();
}
match /yyy/{yyyId} {
allow get: if getYyy(yyyId);
match /zzz/{zzzId} {
allow get: if getZzz(yyyId, zzzId);
}
}
// xxx.rules //
function getXxx() {
return isAuthenticated();
}
// yyy.rules //
function getYyy(yyyId) {
return uidMatches(yyyId);
}
// zzz.rules //
function getZzz(yyyId, zzzId) {
let authUid = getAuthUid();
let reqData = getRequestedData();
return ~ && ~ && ~; // 実際には ~ に何か条件を書いていく
}
// helper.rules //
// @return {boolean}
function isAuthenticated() {
return request.auth != null;
}
// @param {string} id - User id
// @return {boolean}
function uidMatches(id) {
return id == request.auth.uid;
}
// @param {array} paths - e.g. ['collection name', 'document id']
// @return {boolean}
function docExists(paths) {
return exists(getPath(paths));
}
// @return {string} - Authentication UID
function getAuthUid() {
return request.auth.uid;
}
// @return {string} - Authentication Email
function getAuthEmail() {
return request.auth.token.email;
}
// @return {object} - Requested Data
function getRequestedData() {
return request.resource.data;
}
// @return {object} - Resource Data
function getResourceData() {
return resource.data;
}
// @param {array} paths - e.g. ['collection name', 'document id', 'collection name', 'document id']
// @return {path} - Document path
function getPath(paths) {
return path([['databases', database, 'documents'].join('/'), paths.join('/')].join('/'));
}
}
}
以上 #
甘えてないでファイル結合処理のスクリプトぐらい自分で書けば?という意図で Firebase が意識的にファイル分割機能を公式提供していない可能性もある??
なにはともあれこの記事がお役に立てば幸いです!