Firebase Cloud Storageのセキュリティルールのテストを書いてみる
firebase-tools
のバージョン9.11.0でStorage Emulatorがサポートされ、firebase/rule-unit-testing
のバージョン1.3.0から、Storageのセキュリティルールのテストを書くための各種インターフェースがサポートされ、実際のプロジェクトのStorageを操作しなくてもセキュリティルールをテストすることができるようになりました。
また、firebase/rule-unit-testing
バージョン2からは、よりテストが書きやすくなるようにインターフェースが一新されました。
今回は現状あまり情報の少ない、Storageのルールのテストの書き方と簡単な例をご紹介します。
packageのインストール
今回最低限必要なものが以下になります。
"dependencies": {
"firebase": "^9.1.2"
},
"devDependencies": {
"@firebase/rules-unit-testing": "^2.0.1",
"firebase-tools": "^9.19.0", // 任意
"jest": "^27.2.4" // 任意、お好きなものを
},
"@firebase/rules-unit-testing"
バージョン2.0.0以上と"firebase"
バージョン9以上をインストールできると良いでしょう。
また、テストフレームワークに関してはjest
でもmocha
でもお好きなものを採用して構いません。
以降はjestを前提に進めていきます。
storage.rulesの準備
適切な場所にstorage.rules
を配置し、次のようにルールを記述します。
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow get: if request.auth.uid == userId;
allow create: if request.auth.uid == userId
&& isValidContentType(request.resource)
&& isValidFileExtension(request.resource.name)
&& isValidFileSize(request.resource);
}
}
function isValidContentType(data) {
return data.contentType in ['image/png'];
}
function isValidFileExtension(fileName) {
return fileName.matches('.*[.]png');
}
function isValidFileSize(data) {
return data.size < 50 * 1024
}
}
今回は、ユーザーが自身のディレクトリ users/{user_id}/
以下にpng画像をアップロードでき、自分自身の画像のみ取得できることを想定したルールにしています。
また、png画像であることを保証するために、Content-Type,拡張子の確認を行い、追加で50KiB未満の画像であることを合わせて確認します。
ディレクトリ内の画像の一覧を取得、metadataの更新、ファイルの削除は簡略化するために今回許可していません。
Storageのセキュリティルールの基本については以下のドキュメントから学ぶことができます。
firebase.jsonの準備
Storage Emulatorを動かすための設定を、firebase.json
に記述します。
{
"storage": {
"rules": "storage.rules"
},
"emulators": {
"storage": {
"port": 9199
}
}
}
Storage Emulatorのポートはデフォルトで9199
のようなのでそれを指定します。
もしくは firebase init
でセットアップするときにEmulatorのセットアップも合わせて行うか、 firebase emulator:setup
でセットアップするとこの設定が追記されると思います。手動で書き込んでも問題ありません。
テストをセットアップする
Storageのルールのテストに必要なセットアップ処理(と終了処理)を記述します。
import * as ftest from '@firebase/rules-unit-testing'
import * as fs from 'fs'
import 'jest'
let testEnv: ftest.RulesTestEnvironment
beforeAll(async () => {
testEnv = await ftest.initializeTestEnvironment({
projectId: 'demo-users-storage-rules-test',
storage: {
rules: fs.readFileSync('./storage.rules', 'utf8'),
},
})
})
beforeEach(async () => await testEnv.clearStorage())
afterAll(async () => await testEnv.cleanup())
- テスト開始時に、テストファイルごとに一意のprojectIDを指定して、RuleTestEnvironmentを生成する
- それぞれのテストケース開始前(もしくはそれぞれのテストケース終了時)にデータを削除する
- 全てのテストケース終了時に、RulesTestEnvironmentのcleanupをする
といった内容になります。このあたりの記述はFirestoreのセキュリティルールのテストととても似ています。
テストファイルを作成する
(特に)get, list, update, deleteオペレーションのテストを書く場合に、事前にテストファイルをアップロードしておく必要があります。
予めファイルをStorageにアップロードしてテストファイルを準備する場合は、RuleTestEnvironment
のwithSecurityRulesDisabled
関数を利用して、セキュリティルールをバイパス(無効にした状態)してファイルを準備するのが良いでしょう。
例えば、users/user/icon.png
というパスで画像を、セキュリティルールを無視して上げる場合は次のようになります。
const userImageRef = (storage: Storage, userId: string, imageName: string) =>
userImagesRef(storage, userId).child(imageName)
const loadIconImage = () => fs.readFileSync('./icon.png')
beforeEach(async () => {
await testEnv.withSecurityRulesDisabled(async context => {
await userImageRef(context.storage(), userId, 'icon.png').put(loadIconImage()).then()
})
})
非認証、認証済ユーザーのStorageを生成する
- 非認証状態のStorageインスタンスを生成する
testEnv.unauthenticatedContext().storage()
- 認証状態のStorageインスタンスを生成する
testEnv.authenticatedContext('user_id').storage()
指定のユーザーID(auth.uid)を渡して生成します。また第2引数により細かいauthの情報を渡すこともできます。
const storage = testEnv.authenticatedContext(
userId,
{
email 'foobar@baz.com',
email_verified: true
}
).storage()
オペレーション毎のルールの検証
get/listのルールを検証する
getに関しては getDownloadUrl
もしくは getMetadata
で検証が行えます。
const userId = ...
const path = `users/${userId}/icon.png`
const ref = testEnv.authenticatedContext(userId).storage().ref(path)
await assertSucceeds(ref.getDownloadURL())
listに関しては list
もしくは listAll
で検証ができます。
const userId = ...
const ref = testEnv.authenticatedContext(userId).storage().ref(`users/${userId}`)
await assertSucceeds(ref.listAll())
createのルールを検証する
テストファイルを作成するときと同様に、put
を利用します。
const userId = ...
const ref = testEnv.authenticatedContext(userId).storage().ref(
`users/${userId}/icon.png`
)
const image = fs.readFileSync('./icon.png')
await assertSucceeds(
ref.put(image, { contentType: 'image/png' }).then()
)
put
自体はPromise型を返さないため、 .then()
を付けてPromise型にし、awaitで結果を待つようにしています。
もしかしたらasync/awaitではなく通常のassertion+ put
だけでも十分かもしれません。
metadataの検証
アップロードする際のmetadataの情報、例えばcontentType
やカスタムデータなどを検証したい場合は、put
の第2引数を変えると良いでしょう。
await assertFails(
ref.put(image, { contentType: 'image/jpeg' }).then()
)
ファイル拡張子の検証
StorageReference
のpathの拡張子を任意のものに変更してテストを検証します
const userId = ...
const ref = testEnv.authenticatedContext(userId).storage().ref(
`users/${userId}/icon.jpeg`
)
await assertFails(
ref.put(image, { contentType: 'image/png' }).then()
)
ファイルサイズの検証
put
にわたすファイルを指定のサイズのものに変えると良いでしょう。
// Load over 50KiB icon image
const image = fs.readFileSync('./big_icon.png')
updateのルールを検証する
updateのルールは、 updateMetadata
を用いることで検証ができます。
await assertFails(ref.updateMetadata({})
deleteのルールを検証する
deleteのルールは、 delete
を用いることで検証できます。
await assertFails(ref.delete())
💡 大きなファイルをEmulatorにアップロードするには
今回の例では簡略化するためにファイルサイズの検証を50KiBとしていましたが、実際のプロダクトでは数MiB,GiBを扱うかと思います。
その場合を検証するために、リポジトリ内に巨大なファイルを作成して配置し、テストに用いるのは良い案ではありません。
その場合には以下のようにして大きいファイルをテスト内で動的に生成し、利用すると良いでしょう。
const createTestFile = (size: int) => Buffer.alloc(size);
// Create a buffer with 500Mib size and try to upload it to Storage
await assertFails(ref.put(createLargeFile(500 * 1024 * 1024)).then())
テストの全容
今回は記事向けに記述を省いてライトに書いていますが、今回紹介したstorage.rules
のセキュリティルールに対する完全な状態のテストコードはこちらに掲載しています。
テストの実行
テストの実行には、Storage Emulatorの起動が必要になるので、firebaseのコマンドでEmulatorを起動した上で、テストを実行します。
まだStorage Emulatorをセットアップしていない場合は以下のコマンドを先に実行します。
$ yarn firebase setup:emulators:storage
$ yarn firebase emulators:exec --only storage 'jest'
$ yarn firebase emulators:exec --only storage 'jest --watch'
これらのコマンドはpackage.jsonのscripts
に書いておくと良いでしょう。
今回はStorage単体でのテストを想定していますが、もし他のEmulatorと組み合わせたテストを行う場合には各種必要なEmulatorを起動してください。
さいごに
現時点ではそこまで情報が豊富ではないStorageのセキュリティルールのテストを書く方法についてまとめてみました。
サンプルはGitHubに掲載しています。npm
もしくはyarn
がセットアップできていればクローンして2ステップでテストを試せるようにしてあるので気になる方は試してみてください。
また、英語版の記事も書きましたのでよかったら見てください。
併せて読みたい
Discussion