特定のフィールドへのアクセスを制御する

このページでは、セキュリティ ルールの構造化セキュリティ ルールの条件の記述のコンセプトを基に、Cloud Firestore Security Rules を使用して、クライアントがドキュメント内の一部のフィールドのみでオペレーションを実行することを許可するルールを作成する方法について説明します。

ドキュメント レベルではなくフィールド レベルでドキュメントの変更を制御する場合も考えられます。

たとえば、クライアントでドキュメントの作成または変更を許可する一方で、そのドキュメントの特定のフィールドの編集は許可しないことが可能です。また、クライアントが常に作成するドキュメントに、特定のフィールド セットを含める場合も考えられます。このガイドでは、Cloud Firestore Security Rules を使用してこれらのタスクの一部を実施する方法について説明します。

特定のフィールドのみに対する読み取りアクセスを許可する

Cloud Firestore の読み取りはドキュメント レベルで実行されます。ドキュメント全体を取得するか、何も取得しないかのどちらかです。部分的にドキュメントを取得する方法はありません。セキュリティ ルールのみを使用して、ユーザーによるドキュメント内の特定のフィールドの読み取りを禁止することはできません。

ドキュメント内の特定のフィールドが一部のユーザーに表示されないようにする場合は、そのフィールドを別のドキュメントに配置するのが最適です。たとえば、次のように private サブコレクションでドキュメントを作成できます。

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

次に、2 つのコレクションに対して異なるアクセスレベルを持つセキュリティ ルールを追加できます。この例では、カスタム認証クレームを使用して、カスタム認証クレームの roleFinance と等しいユーザーのみが従業員の財務情報を表示できるようにしています。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

ドキュメント作成時にフィールドを制限する

Cloud Firestore はスキーマレスです。つまり、ドキュメントに含まれるフィールドのデータベース レベルでの制限はありません。この柔軟性により開発は容易になりますが、クライアントが作成できるのは、特定のフィールドを含むドキュメント、または他のフィールドを含まないドキュメントのみに制限したい場合もあります。

こうしたルールを作成するには、request.resource.data オブジェクトの keys メソッドを使用します。これは、クライアントがこの新しいドキュメントに書き込もうとしているすべてのフィールドのリストです。このフィールド セットと hasOnly()hasAny() などの関数を組み合わせることで、ユーザーが Cloud Firestore に追加できるドキュメントの種類を制限するロジックを加えることができます。

新しいドキュメントの特定のフィールドを必須にする

restaurant コレクション内に作成されたすべてのドキュメントに、少なくとも name フィールド、location フィールド、city フィールドが必ず含まれるようにするとします。このためには、新しいドキュメントのキーのリストで hasAll() を呼び出します。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

これにより、他のフィールドも含めてレストランを作成できるようになりますが、クライアントが作成したすべてのドキュメントに少なくともこれら 3 つのフィールドが含まれるようになります。

新しいドキュメントで特定のフィールドを禁止する

同様に、クライアントが特定のフィールドを含むドキュメントを作成できないようにすることもできます。これを行うには、禁止フィールドのリストに対して hasAny() を使用します。ドキュメントにこれらのフィールドが 1 つでも含まれている場合、このメソッドは true と評価されるため、特定のフィールドを除外するにはこの結果を反転させます。

たとえば次の例では、クライアントは average_score フィールドまたは rating_count フィールドを含むドキュメントを作成することはできません。これらのフィールドは後でサーバー呼び出しによって追加されるためです。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

新しいドキュメントのフィールドの許可リストを作成する

新しいドキュメント内の特定のフィールドを禁止するのではなく、新しいドキュメントで明示的に許可されているフィールドだけのリストを作成することもできます。ここで hasOnly() 関数を使用すると、作成された新しいドキュメントにこれらのフィールド(またはフィールドのサブセット)のみが含まれるようになります。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

必須フィールドと省略可能なフィールドを組み合わせる

セキュリティ ルールで hasAll オペレーションと hasOnly オペレーションを組み合わせると、一部のフィールドを必須にして、その他のフィールドは許可することができます。たとえば、次の例では、すべての新しいドキュメントで name フィールド、location フィールド、city フィールドを必須にして、オプションとして address フィールド、hours フィールド、cuisine フィールドを許可します。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

実際のシナリオでは、次に示すように、このロジックをヘルパー関数に移動することで、コードの重複を避け、省略可能なフィールドと必須フィールドを 1 つのリストに簡単に統合できます。

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

更新時にフィールドを制限する

一般的なセキュリティ慣行として、クライアントが一部のフィールドのみ編集できるようにし、他のフィールドは編集できないようにするという方法があります。これは、前のセクションで説明した request.resource.data.keys() のリストだけでは達成できません。このリストは更新後を参照するので、クライアントが変更しなかったフィールドも含めて完全なドキュメントを表すためです。

ただし、diff() 関数を使用した場合は、request.resource.data を、更新前のデータベース内のドキュメントを表す resource.data オブジェクトと比較できます。これにより、2 つのマップ間のすべての変更を含むオブジェクトである mapDiff オブジェクトが作成されます。

この mapDiff で affectedKeys() メソッドを呼び出すと、編集で変更されたフィールドのセットを取得できます。その後、hasOnly()hasAny() などの関数を使用して、このセットに特定の項目が含まれている(または含まれていない)ことを確認できます。

一部のフィールドの変更を禁止する

affectedKeys() で生成されたセットの hasAny() メソッドを使用し、その結果を反転させることで、変更されたくないフィールドへの変更を試みるクライアント リクエストを拒否できます。

たとえば、クライアントにレストランに関する情報の更新を許可しつつ、平均スコアやレビュー数の変更は許可しないようにすることができます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

特定のフィールドのみの変更を許可する

変更を加えられたくないフィールドを指定する代わりに、hasOnly() 関数を使用して、変更されるべきフィールドのリストを指定することもできます。そのほうが一般的に安全であると考えられます。セキュリティ ルールで明示的に許可しない限り、新しいドキュメント フィールドへの書き込みはデフォルトで禁止されるためです。

たとえば、average_score フィールドと rating_count フィールドを禁止するのではなく、クライアントが name フィールド、location フィールド、city フィールド、address フィールド、hours フィールド、cuisine フィールドだけを変更できるセキュリティ ルールを作成できます。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

つまり、アプリの今後の反復処理で、レストランのドキュメントに telephone フィールドが含まれている場合、前に戻ってセキュリティ ルールの hasOnly() リストにそのフィールドを追加しない限り、このフィールドを編集する試みは失敗します。

フィールド タイプを適用する

Cloud Firestore がスキーマレスであることのもう一つの利点は、特定のフィールドに格納できるデータ型が、データベース レベルで適用されないことです。これはセキュリティ ルールで適用できますが、is 演算子を使用する必要があります。

たとえば、次のセキュリティ ルールでは、レビューの score フィールドが整数であること、headline フィールド、content フィールド、author_name フィールドが文字列であること、review_date がタイムスタンプであることを指定します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

is 演算子の有効なデータ型は、boolbytesfloatintlistlatlngnumberpathmapstringtimestamp です。is 演算子は constraintdurationsetmap_diff のデータ型もサポートしますが、これらはセキュリティ ルールの言語自体によって生成され、クライアントにより生成されないので、実際のアプリケーションではほとんど使用しません。

list データ型と map データ型は、汎用型または型引数をサポートしていません。つまり、セキュリティ ルールを使用して、特定のフィールドにリストやマップが含まれるように指定できますが、フィールドにすべての整数のリストまたはすべての文字列のリストが含まれるように指定することはできません。

同様に、セキュリティ ルールを使用してリスト(角かっこ表記を使用)やマップ(キー名を使用)の特定のエントリに型の値を適用することもできますが、マップやリストのすべてのメンバーのデータ型を一度に適用できる簡単な方法はありません。

たとえば、次のルールにより、ドキュメントの tags フィールドにリストが含まれ、最初のエントリが文字列になります。また、product フィールドに、文字列である商品名と、整数である数量を含むマップが含まれるようになります。

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

フィールド タイプは、ドキュメントの作成時と更新時に適用する必要があります。そのため、セキュリティ ルールの create セクションと update セクションの両方で呼び出すヘルパー関数の作成を検討することをおすすめします。

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

省略可能なフィールドのタイプを適用する

foo が存在しないドキュメントで request.resource.data.foo を呼び出すとエラーになり、呼び出しを行うセキュリティ ルールがリクエストを拒否することに注意してください。この状況に対処するには、request.resource.dataget メソッドを使用します。get メソッドを使用すると、マップから取得するフィールドが存在しない場合に、そのフィールドに対してデフォルトの引数を指定できます。

たとえば、レビュー ドキュメントに省略可能な photo_url フィールドと tags フィールドも含まれ、それぞれ文字列とリストであることを検証する場合は、reviewFieldsAreValidTypes 関数を次のように書き換えます。

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

これは、tags が存在するものの、リストでないドキュメントを拒否しますが、tags(または photo_url)フィールドを含まないドキュメントは許可します。

部分的な書き込みは許可されない

最後の注意事項として、Cloud Firestore Security Rules は、クライアントにドキュメントの変更を許可するか、編集全体を拒否するかのどちらかです。ドキュメント内の一部のフィールドへの書き込みを許可する一方で、同じオペレーションにおいてその他の書き込みを拒否するようなセキュリティ ルールを作成することはできません。