Realtime Database のセキュリティ ルール言語の基本構文を学習する

Firebase Realtime Database セキュリティ ルールを使用すると、データベースに保存されているデータへのアクセスを制御できます。ルールの構文は柔軟なので、データベースに対するすべての書き込みオペレーションから個別のノードに対するオペレーションまで、あらゆるオペレーションに一致するルールを作成できます。

Realtime Database セキュリティ ルールはデータベースの宣言型の構成です。つまり、ルールはプロダクトのロジックとは別に定義されます。これには次のようないくつかの利点があります。クライアントがセキュリティ実施の責任を担う必要がないこと。バグのある実装によりデータを危険にさらすことがなくなること。そして最も重要なこととして、照会サーバーなどを介在させることなくデータを外部から保護できるという利点もあります。

このトピックでは、完全なルールセットの作成に使用される Realtime Database セキュリティ ルールの基本的な構文と構造について説明します。

セキュリティ ルールの構造化

Realtime Database セキュリティ ルールは JSON ドキュメントに含まれている JavaScript に似た式で構成されます。ルールの構造はデータベースに保存しているデータの構造に従う必要があります。

基本ルールでは、保護するノードのセット、関連するアクセス方法(読み取り、書き込みなど)、アクセスを許可または拒否する条件を特定します。 以下の例では、条件は単純な true ステートメントと false ステートメントですが、次のトピックでは、条件を表現するためのより動的な方法について説明します。

たとえば child_nodeparent_node の下で保護する場合、従うべき一般的な構文は次のようになります。

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

このパターンを適用します。たとえば、メッセージのリストを追跡していて、次のようなデータがあるとします。

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

これに対応するルールも同様に構造化する必要があります。このデータ構造に適した読み取り専用セキュリティのルールセットを次に示します。この例では、ルールの適用先にするデータベース ノードを指定する方法と、それらのノードでルールを評価する条件を指定する方法を示します。

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

基本ルールのオペレーション

データに対して実行されるオペレーションの種類に応じて、セキュリティを適用するためのルールは 3 種類(.write.read.validate)あります。次に、その用途を簡潔に示します。

ルールの種類
.read ユーザーによるデータの読み取りを許可するときに記述します。
.write ユーザーによるデータの書き込みを許可するときに記述します。
.validate 値の適切なフォーマット方法、子属性を持つかどうか、およびデータ型を定義します。

ワイルドカード キャプチャ変数

すべてのルール ステートメントはノードをポイントします。ステートメントは、特定のノードをポイントしたり、$ ワイルドカード キャプチャ変数を使用して階層レベルでノードセットをポイントしたりすることができます。これらのキャプチャ変数を使用してノードキーの値を保存し、後続のルール ステートメント内で使用します。この手法を使用すると、より複雑なルールの条件を記述できます。これについては、次のトピックで詳しく説明します。

{
  "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 }
    }
  }
}

読み取りルールと書き込みルールのカスケード

.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 は返されないと考えるかもしれません。しかし、実際の結果はエラーになります。

JavaScript
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
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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 コンソールのセキュリティ シミュレータでこのルールを評価すると、読み取りオペレーションが拒否されたことがわかります。これは、/records/ パスに対する読み取りアクセスを許可するルールが存在しなかったためです。ただし、リクエストしたパスに rec1 のルールが存在しなかったため、評価されることがなかったことに注意してください。rec1 をフェッチするには、それに直接アクセスする必要があります。

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
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!

重複するステートメント

1 つのノードに複数のルールを適用できます。複数のルール式でノードを識別する場合、いずれかの条件が false の場合、アクセス方法は拒否されます。

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

上の例では、message1 ノードへの読み取りは拒否されます。これは、最初のルールが常に true になっても、2 番目のルールが常に false になるためです。

次のステップ

Firebase Realtime Database セキュリティ ルールについての理解を深めるには、以下をご覧ください。

  • ルール言語の次の主要なコンセプトである動的条件について学習します。動的条件では、ルールにより、ユーザー承認の確認、既存データと受信データの比較、受信データの検証、クライアントから受信したクエリの構造の確認などを行うことができます。

  • 一般的なセキュリティのユースケースと、それらのユースケースに対処する Firebase セキュリティ ルールの定義を確認します。