このガイドは、コアとなる Firebase セキュリティ ルール言語を学習するためのガイドに基づいて、Firebase Realtime Database セキュリティ ルールに条件を追加する方法について説明します。
Realtime Database セキュリティ ルールの主な構成要素は条件です。条件は、特定のオペレーションを許可するか拒否するかを決定するブール式です。基本的なルールとしては、リテラル true
および false
を条件として使用すれば、十分な機能を発揮します。ただし、Realtime Database セキュリティ ルール言語では、より複雑な条件を記述でき、次の操作を行うことができます。
- ユーザー認証を確認する
- 新しく送信されたデータと照合して既存のデータを評価する
- データベースのさまざまな部分にアクセスし、それらを比較する
- 受信データを検証する
- セキュリティ ロジックに受信クエリの構造を使用する
$ 変数を使用したパスセグメントのキャプチャ
$
接頭辞を持つキャプチャ変数を宣言することにより、読み取りや書き込みのパスの一部をキャプチャできます。これはワイルドカードとして機能し、ルールの条件の内部で使用するために該当のキーの値を保存します。
{ "rules": { "rooms": { // this rule applies to any child of /rooms/, the key for each room id // is stored inside $room_id variable for reference "$room_id": { "topic": { // the room's topic can be changed if the room id has "public" in it ".write": "$room_id.contains('public')" } } } } }
動的な $
変数は定数値のパス名とともに使用することもできます。次の例では、$other
変数を使用して .validate
ルールを宣言します。このルールにより、widget
には title
と color
以外に子が存在しないようになります。子が追加で作成される結果になるあらゆる書き込みは失敗します。
{ "rules": { "widget": { // a widget can have a title or color attribute "title": { ".validate": true }, "color": { ".validate": true }, // but no other child paths are allowed // in this case, $other means any key excluding "title" and "color" "$other": { ".validate": false } } } }
認証
最も一般的なセキュリティ ルールのパターンの一つは、ユーザーの認証状態に基づいてアクセスを制御することです。たとえば、アプリにログインしているユーザーのみがデータの書き込みを許可されるようにすることができます。
Firebase Authentication を使用するアプリの場合、request.auth
変数には、データを要求しているクライアントの認証情報が含まれています。request.auth
の詳細については、リファレンス ドキュメントをご覧ください。
Firebase Authentication は Firebase Realtime Database との統合により、条件を使用してユーザーごとにデータアクセスを制御できます。ユーザーが認証されると、Realtime Database セキュリティ ルールの auth
変数にユーザーの情報が設定されます。この情報には、一意の識別子(uid
)に加え、Facebook の ID やメールアドレスなど、リンクされたアカウント データ、およびその他の情報が含まれます。カスタムの auth プロバイダを実装する場合は、独自のフィールドをユーザーの auth ペイロードに追加できます。
このセクションでは、Firebase Realtime Database セキュリティ ルール言語をユーザーの認証情報と組み合わせる方法について説明します。これらの 2 つのコンセプトを組み合わせることにより、ユーザー ID に基づいたデータへのアクセス制御が可能になります。
auth
変数
ルール内で事前定義された auth
変数が null の場合に、認証は実行されます。
ユーザーが Firebase Authentication で認証されると、この変数には次の属性が含まれています。
provider | 使用された認証方法(「password」、「anonymous」、「facebook」、「github」、「google」、または「twitter」)。 |
uid | 一意のユーザー ID。すべてのプロバイダにわたって一意であることが保証されています。 |
token |
Firebase Auth ID トークンのコンテンツ。詳細については、auth.token のリファレンス ドキュメントをご覧ください。 |
次に、auth
変数を使用して各ユーザーがユーザー固有のパスのみに書き込みできるようにするサンプルのルールを示します。
{ "rules": { "users": { "$user_id": { // grants write access to the owner of this user account // whose uid must exactly match the key ($user_id) ".write": "$user_id === auth.uid" } } } }
認証条件をサポートするようにデータベースを構造化する
通常は、ルールの書き込みを容易にする方法でデータベースを構造化すると便利です。ユーザーデータを Realtime Database に保存するための一般的なパターンの一つは、子がすべてのユーザーの uid
値である単一の users
ノードにすべてのユーザーを保存することです。このデータへのアクセスを制限してログイン ユーザーのみが自分のデータを参照できるようにしたい場合、ルールは次のようになります。
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
認証カスタム クレームの操作
さまざまなユーザーのカスタム アクセス制御が必要なアプリの場合、Firebase Authentication を使用すると、デベロッパーは Firebase ユーザーに対してクレームを設定できます。これらのクレームには、ルールの auth.token
変数でアクセスできます。hasEmergencyTowel
カスタム クレームを使用するルールの例を次に示します。
{ "rules": { "frood": { // A towel is about the most massively useful thing an interstellar // hitchhiker can have ".read": "auth.token.hasEmergencyTowel === true" } } }
独自のカスタム認証トークンを作成するデベロッパーは、オプションでこれらのトークンにクレームを追加できます。これらのクレームは、ルールの auth.token
変数で使用できます。
既存のデータと新規のデータ
事前定義された data
変数は、書き込みオペレーションが行われる前のデータの参照に使用されます。一方、newData
変数には書き込みオペレーションが成功した場合に存在することになる新規のデータが含まれています。newData
は書き込まれた新しいデータと既存のデータを統合した結果を表します。
例として、次のルールでは、新しいレコードの作成や既存のレコードの削除はできますが、既存の null 以外のデータに変更を加えることはできません。
// we can write as long as old data or new data does not exist // in other words, if this is a delete or a create, but not an update ".write": "!data.exists() || !newData.exists()"
その他のパスにあるデータの参照
任意のデータをルールの基準として使用できます。事前定義された変数 root
、data
、newData
を使用すると、書き込みイベントの前後に存在するとおりに任意のパスにアクセスできます。
次の例を見てみましょう。この例では、/allow_writes/
ノードの値が true
で、親ノードに readOnly
フラグが設定されておらず、新しく書き込まれたデータに foo
という名前の子がある限り、書き込みオペレーションが許可されます。
".write": "root.child('allow_writes').val() === true && !data.parent().child('readOnly').exists() && newData.child('foo').exists()"
データの検証
データ構造の適用や、データのフォーマットとコンテンツの検証は、.validate
ルールを使用して実行する必要があります。このルールは、.write
ルールでアクセス権の付与に成功した後にのみ実行されます。次に、.validate
ルールのサンプル定義を示します。このルールでは、1900~2099 年までの YYYY-MM-DD 形式の日付のみが許可されます。これは正規表現を使用してチェックされます。
".validate": "newData.isString() && newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"
.validate
ルールは、カスケード処理されない唯一のセキュリティ ルールです。ある子レコードで検証ルールが失敗した場合、書き込みオペレーション全体が拒否されます。また、データが削除されるとき(つまり、書き込まれる新しい値が null
のとき)には、検証の定義は無視されます。
これは些細なことのように思えますが、実際には強力な Firebase Realtime Database セキュリティ ルールを記述するための重要な機能です。次のルールを見てみましょう。
{ "rules": { // write is allowed for all paths ".write": true, "widget": { // a valid widget must have attributes "color" and "size" // allows deleting widgets (since .validate is not applied to delete rules) ".validate": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99 ".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical // /valid_colors/ index ".validate": "root.child('valid_colors/' + newData.val()).exists()" } } } }
このパターンに留意して、次の書き込みオペレーションの結果を見てみましょう。
JavaScript
var ref = db.ref("/widget"); // PERMISSION_DENIED: does not have children color and size ref.set('foo'); // PERMISSION DENIED: does not have child color ref.set({size: 22}); // PERMISSION_DENIED: size is not a number ref.set({ size: 'foo', color: 'red' }); // SUCCESS (assuming 'blue' appears in our colors list) ref.set({ size: 21, color: 'blue'}); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"]; // PERMISSION_DENIED: does not have children color and size [ref setValue: @"foo"]; // PERMISSION DENIED: does not have child color [ref setValue: @{ @"size": @"foo" }]; // PERMISSION_DENIED: size is not a number [ref setValue: @{ @"size": @"foo", @"color": @"red" }]; // SUCCESS (assuming 'blue' appears in our colors list) [ref setValue: @{ @"size": @21, @"color": @"blue" }]; // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate [[ref child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget") // PERMISSION_DENIED: does not have children color and size ref.setValue("foo") // PERMISSION DENIED: does not have child color ref.setValue(["size": "foo"]) // PERMISSION_DENIED: size is not a number ref.setValue(["size": "foo", "color": "red"]) // SUCCESS (assuming 'blue' appears in our colors list) ref.setValue(["size": 21, "color": "blue"]) // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("widget"); // PERMISSION_DENIED: does not have children color and size ref.setValue("foo"); // PERMISSION DENIED: does not have child color ref.child("size").setValue(22); // PERMISSION_DENIED: size is not a number Map<String,Object> map = new HashMap<String, Object>(); map.put("size","foo"); map.put("color","red"); ref.setValue(map); // SUCCESS (assuming 'blue' appears in our colors list) map = new HashMap<String, Object>(); map.put("size", 21); map.put("color","blue"); ref.setValue(map); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size curl -X PUT -d 'foo' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION DENIED: does not have child color curl -X PUT -d '{"size": 22}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION_DENIED: size is not a number curl -X PUT -d '{"size": "foo", "color": "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # SUCCESS (assuming 'blue' appears in our colors list) curl -X PUT -d '{"size": 21, "color": "blue"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # If the record already exists and has a color, this will # succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) # will fail to validate curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
ここで、同じ構造を見てみましょう。ただし、.validate
の代わりに .write
ルールを使用しています。
{ "rules": { // this variant will NOT allow deleting records (since .write would be disallowed) "widget": { // a widget must have 'color' and 'size' in order to be written to this path ".write": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical valid_colors/ index // BUT ONLY IF WE WRITE DIRECTLY TO COLOR ".write": "root.child('valid_colors/'+newData.val()).exists()" } } } }
このパターンでは、以下のオペレーションはいずれも成功します。
JavaScript
var ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.set({size: 99999, color: 'red'}); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL]; // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored [ref setValue: @{ @"size": @9999, @"color": @"red" }]; // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") [[ref childByAppendingPath:@"size"] setValue: @99];
Swift
var ref = Firebase(url:URL) // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.setValue(["size": 9999, "color": "red"]) // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored Map<String,Object> map = new HashMap<String, Object>(); map.put("size", 99999); map.put("color", "red"); ref.setValue(map); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size, # so write is allowed and the .write rule under color is ignored curl -X PUT -d '{size: 99999, color: "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # ALLOWED? Works even if widget does not exist, allowing us to create a widget # which is invalid and does not have a valid color. # (allowed by the write rule under "color") curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
これは .write
ルールと .validate
ルールとの相違点について示しています。示したとおり、これらのルールはすべて .validate
を使用して記述する必要がありますが、newData.hasChildren()
ルールに限っては例外となる場合があります。これは削除が許可されている必要があるかどうかに応じて異なります。
クエリベースのルール
ルールをフィルタとして使用することはできませんが、ルール内のクエリ パラメータを使用してデータのサブセットへのアクセスを制限できます。ルール内で query.
式を使用し、クエリ パラメータに基づいて読み取りまたは書き込みアクセス権を付与します。
たとえば、次のクエリベースのルールでは、ユーザーベースのセキュリティ ルールとクエリベースのルールを使用して、baskets
コレクション内のデータへのアクセスを、アクティブ ユーザーが所有する買い物かごのみに制限します。
"baskets": {
".read": "auth.uid !== null &&
query.orderByChild === 'owner' &&
query.equalTo === auth.uid" // restrict basket access to owner of basket
}
次の、ルール内のクエリ パラメータを含むクエリは、成功します。
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
しかし、ルール内のパラメータを含まないクエリは、PermissionDenied
エラーで失敗します。
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
クエリベースのルールを使用して、クライアントが読み取りオペレーションでダウンロードできるデータの量を制限することもできます。
たとえば次のルールでは、読み取りアクセスの対象を、優先度順に並べ替えられた最初の 1,000 件のクエリ結果のみに制限します。
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
// Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
Realtime Database セキュリティ ルールでは次の query.
式を利用できます。
クエリベースのルールの式 | ||
---|---|---|
式 | 型 | 説明 |
query.orderByKey query.orderByPriority query.orderByValue |
ブール値 | キー、優先度、または値で並べ替えられたクエリの場合は True です。それ以外の場合は false。 |
query.orderByChild | 文字列 null |
文字列で子ノードへの相対パスを表します。例: query.orderByChild === "address/zip" クエリが子ノードで並べ替えられていない場合、この値は null です。 |
query.startAt query.endAt query.equalTo |
文字列 数値 ブール値 null |
実行中のクエリの範囲を取得するか、または範囲が設定されていない場合は null を返します。 |
query.limitToFirst query.limitToLast |
数値 null |
実行中のクエリの制限を取得するか、または制限が設定されていない場合は null を返します。 |
次のステップ
条件についての以上の解説により、ルールについての理解を深めることができれば、次の学習に進む準備できています。
コアとなるユースケースの対処方法、およびルールを開発、テスト、デプロイするためのワークフローについて学習する。
- 条件の作成に使用できる定義済みルールの変数の完全なセットについて学習する。
- 一般的なシナリオに対処するルールを記述する。
- 安全でないルールを見つけて回避する必要がある状況を確認することで、学習内容に基づいてルールを作成する。
- Firebase Local Emulator Suite の概要、およびこのスイートを使用してルールをテストする方法について学習する。
- ルールのデプロイに利用できる方法を確認する。
Realtime Database に固有であるルールの機能について学習する。
- Realtime Database にインデックスを付ける方法について学習する。
- ルールをデプロイするための REST API を確認する。