Realtime Database セキュリティ ルールで条件を使用する

このガイドは、コアとなる 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 には titlecolor 以外に子が存在しないようになります。子が追加で作成される結果になるあらゆる書き込みは失敗します。

{
  "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 AuthenticationFirebase Realtime Database と統合されており、条件を使用してユーザーごとにデータアクセスを制御できます。ユーザーが認証されると、Realtime Database セキュリティ ルールの auth 変数にユーザーの情報が設定されます。この情報には、一意の識別子(uid)に加え、Facebook の ID やメールアドレスなど、リンクされたアカウント データ、およびその他の情報が含まれます。カスタムの認証プロバイダを実装する場合は、独自のフィールドをユーザーの認証ペイロードに追加できます。

このセクションでは、Firebase Realtime Database セキュリティ ルール言語をユーザーの認証情報と組み合わせる方法について説明します。これらの 2 つのコンセプトを組み合わせることにより、ユーザー ID に基づいたデータへのアクセス制御が可能になります。

auth 変数

ルール内で事前定義された auth 変数が null の場合に、認証は実行されます。

ユーザーが Firebase Authentication で認証されると、この変数には次の属性が含まれています。

プロバイダ 使用された認証方法(「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"
      }
    }
  }
}

認証条件をサポートするようにデータベースを構造化する

通常は、Rules の書き込みを容易にする方法でデータベースを構造化すると便利です。ユーザーデータを 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()"

その他のパスにあるデータの参照

任意のデータをルールの基準として使用できます。事前定義された変数 rootdatanewData を使用すると、書き込みイベントの前後に存在するとおりに任意のパスにアクセスできます。

次の例を見てみましょう。この例では、/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
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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 プロダクトは、App Clip ターゲットでは使用できません。
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
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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 を返します。

次のステップ

条件についての以上の解説により、Rules についての理解を深めることができれば、次の学習に進む準備できています。

コアとなるユースケースの対処方法、および Rules を開発、テスト、デプロイするためのワークフローについて学習する。

Realtime Database に固有の Rules の機能について学習する。