Realtime Database ルールはデータベースの宣言型の構成です。つまり、ルールはプロダクトのロジックとは別に定義されます。これには次のようないくつかの利点があります。クライアントがセキュリティの適用を担う必要がないこと。バグのある実装でデータが危殆化することがないこと。そして最も重要なこととして、照会サーバーなどを介在させることなくデータを外部から保護できるという利点もあります。
ルールの構造化
Realtime Database ルールは JSON ドキュメントに含まれている JavaScript に似た式で構成されます。ルールの構造はデータベースに保存しているデータの構造に従う必要があります。たとえば、メッセージのリストを追跡していて、次のようなデータがあるとします。
{ "messages": { "message0": { "content": "Hello", "timestamp": 1405704370369 }, "message1": { "content": "Goodbye", "timestamp": 1405704395231 }, ... } }
これに対応するルールも同様に構造化する必要があります。このデータ構造に適したルールセットの例を次に示します。
{ "rules": { "messages": { "$message": { // only messages from the last ten minutes can be read ".read": "data.child('timestamp').val() > (now - 600000)", // new messages must have a string content and a number timestamp ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()" } } } }
セキュリティ ルールの種類
セキュリティを適用するためのルールは .write
、.read
、.validate
の 3 種類です。次に、その用途を簡潔に示します。
ルールの種類 | |
---|---|
.read | ユーザーによるデータの読み取りを許可するときに記述します。 |
.write | ユーザーによるデータの書き込みを許可するときに記述します。 |
.validate | 値の適切なフォーマット方法、子属性を持つかどうか、およびデータ型を定義します。 |
事前定義された変数
いくつかの便利な変数があらかじめ定義されており、セキュリティ ルールの定義内でアクセスできるようになっています。以下の例では、その変数の大半を使用しています。次に、それぞれの概要と適切な API リファレンスへのリンクを示します。
事前定義された変数 | |
---|---|
now | Linux エポック時刻から現在までの経過時間(ミリ秒)。これは、SDK の firebase.database.ServerValue.TIMESTAMP で作成したタイムスタンプの検証に特に役に立ちます。 |
root | Firebase データベース内のルートパスを表す RuleDataSnapshot。これは試みるオペレーションの前に存在したとおりになります。 |
newData | データを表す RuleDataSnapshot。このデータは試みるオペレーションの後に存在することになるとおりになります。これには、書き込まれる新しいデータと既存のデータが含まれます。 |
data | データを表す RuleDataSnapshot。このデータは試みるオペレーションの前に存在したとおりになります。 |
$ variables | ID と動的な子キーを表すために使用するワイルドカード パス。 |
auth | 認証されたユーザーのトークン ペイロードを表します。 |
これらの変数はルール内の任意の場所で使用できます。たとえば、次のセキュリティ ルールは、/foo/
ノードに書き込まれるデータが 100 文字未満の文字列であることを保証します。
{ "rules": { "foo": { // /foo is readable by the world ".read": true, // /foo is writable by the world ".write": true, // data written to /foo must be a string less than 100 characters ".validate": "newData.isString() && newData.val().length < 100" } } }
既存のデータと新規のデータ
事前定義された 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()"
読み取りルールと書き込みルールのカスケード
.read
と .write
ルールはトップダウンで機能するため、浅いパスのルールが深いパスのルールよりも優先されます。ルールは、特定のパスで読み取り権限や書き込み権限を付与した場合、その下位にあるすべての子ノードへのアクセス権も付与します。 次の構造を見てみましょう。
{ "rules": { "foo": { // allows read to /foo/* ".read": "data.child('baz').val() === true", "bar": { /* ignored, since read was allowed already */ ".read": false } } } }
このセキュリティ構造では、/foo/
に値が true
の子 baz
が含まれている場合に、/bar/
を読み取ることができます。ここでは、/foo/bar/
の下にある ".read": false
ルールは影響しません。子パスではアクセス権を取り消すことができないからです。
直観的ではないかもしれませんが、これはルール言語の強力な利点であり、これにより大変複雑なアクセス権限を最小限の労力で実装できます。これについては、このガイドで後ほどユーザーベースのセキュリティについて説明するときに説明します。
.validate
ルールはカスケード式に適用されるわけではないことに注意してください。書き込みが許可されるには、階層のすべてのレベルですべての検証ルールを満たす必要があります。
ルールはフィルターではない
ルールはアトミック(不可分)な方法で適用されます。つまり、該当の場所にも親の場所にもアクセス権を付与するルールが存在しなかった場合、直ちに、読み取りオペレーションや書き込みオペレーションは失敗します。影響するすべての子パスにアクセス可能な場合でも、親の場所での読み取りは完全に失敗します。次の構造を見てみましょう。
{ "rules": { "records": { "rec1": { ".read": true }, "rec2": { ".read": false } } } }
ルールがアトミックに評価されることを理解していない場合、/records/
パスをフェッチすると rec1
が返される一方で rec2
は返されないと考えるかもしれません。しかし、実際の結果はエラーになります。
JaveScript
var db = firebase.database(); db.ref("records").once("value", function(snap) { // success method is not called }, function(err) { // error callback triggered with PERMISSION_DENIED });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // success block is not called } withCancelBlock:^(NSError * _Nonnull error) { // cancel block triggered with PERMISSION_DENIED }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in // success block is not called }, withCancelBlock: { error in // cancel block triggered with PERMISSION_DENIED })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { // success method is not called } @Override public void onCancelled(FirebaseError firebaseError) { // error callback triggered with PERMISSION_DENIED }); });
REST
curl https://docs-examples.firebaseio.com/rest/records/ # response returns a PERMISSION_DENIED error
/records/
での読み取りオペレーションはアトミックである上、/records/
の下にあるすべてのデータへのアクセス権を付与する読み取りルールが存在しないため、PERMISSION_DENIED
エラーがスローされます。Firebase コンソールのセキュリティ シミュレータでこのルールを評価すると、読み取りオペレーションが拒否されたことがわかります。
Attempt to read /records with auth=Success(null) / /records No .read rule allowed the operation. Read was denied.
/records/
パスに対する読み取りアクセスを許可するルールが存在しなかったため、このオペレーションは拒否されました。ただし、リクエストしたパスに rec1
のルールが存在しなかったため、評価されることがなかったことに注意してください。rec1
をフェッチするには、それに直接アクセスする必要があります。
JaveScript
var db = firebase.database(); db.ref("records/rec1").once("value", function(snap) { // SUCCESS! }, function(err) { // error callback is not called });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // SUCCESS! }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in // SUCCESS! })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records/rec1"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { // SUCCESS! } @Override public void onCancelled(FirebaseError firebaseError) { // error callback is not called } });
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1 # SUCCESS!
クエリベースのルール
ルールをフィルタとして使用することはできませんが、ルール内のクエリ パラメータを使用してデータのサブセットへのアクセスを制限できます。ルール内で 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 を返します。 |
データの検証
データ構造の適用や、データのフォーマットとコンテンツの検証は、.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()" } } } }
このパターンに留意して、次の書き込みオペレーションの結果を見てみましょう。
JaveScript
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()" } } } }
このパターンでは、以下のオペレーションはいずれも成功します。
JaveScript
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()
ルールに限っては例外となる場合があります。これは削除が許可されている必要があるかどうかに応じて異なります。
$ Variables を使用したパスセグメントのキャプチャ
$
接頭辞を持つキャプチャ変数を宣言することにより、読み取りや書き込みのパスの一部をキャプチャできます。これはワイルドカードとして機能し、ルール宣言の内部で使用するために該当のキーの値を保存します。
{ "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 } } } }
匿名チャットの例
ルールをまとめて、セキュアな匿名チャット アプリケーションを作成してみましょう。次の例では、ルールをリストすることに加え、動作可能なバージョンのチャットが含まれます。
{ "rules": { // default rules are false if not specified // setting these to true would make ALL CHILD PATHS readable/writable // ".read": false, // ".write": false, "room_names": { // the room names can be enumerated and read // they cannot be modified since no write rule // explicitly allows this ".read": true, "$room_id": { // this is just for documenting the structure of rooms, since // they are read-only and no write rule allows this to be set ".validate": "newData.isString()" } }, "messages": { "$room_id": { // the list of messages in a room can be enumerated and each // message could also be read individually, the list of messages // for a room cannot be written to in bulk ".read": true, // room we want to write a message to must be valid ".validate": "root.child('room_names/'+$room_id).exists()", "$message_id": { // a new message can be created if it does not exist, but it // cannot be modified or deleted ".write": "!data.exists() && newData.exists()", // the room attribute must be a valid key in room_names/ (the room must exist) // the object to write must have a name, message, and timestamp ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])", // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin" "name": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')" }, // the message must be longer than 0 chars and less than 50 "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" }, // messages cannot be added in the past or the future // clients should use firebase.database.ServerValue.TIMESTAMP // to ensure accurate timestamps "timestamp": { ".validate": "newData.val() <= now" }, // no other fields can be included in a message "$other": { ".validate": false } } } } } }
次のステップ
- セキュリティ ルールを使用したユーザーデータを保護する方法について学ぶ。
- ルールを使用したインデックスの指定について詳しく学ぶ。