20
16

More than 5 years have passed since last update.

Firestoreセキュリティルールで存在するドキュメントの上書き作成を防ぐ

Posted at

今回は、すでに存在するドキュメントの作成を禁止する方法を紹介します。
なぜUpdateでなく「すでに存在するドキュメントの上書き作成」なんて回りくどい言い方をするかというと、

  • クライアントサイドのsetメソッドでの同一パスへの書き込みはUpdate扱いとなる
  • 部分的なフィールドのUpdateは許可したい

といった背景があります。

セキュリティルールで意図しない書込みを防ぐ

Firebase Firestoreのセキュリティルールでは、match文でドキュメントパスを指定し、
allow式でドキュメントのreadcreateupdatedelete権限を指定することができます。
※以下、権限に関するcreate等は太字で表現します。

参考:【改訂版】 Firebase Cloud Firestore rules tips

setメソッドでの同一パスへの書き込みはUpdate扱いとなる

クライアントからのFirestoreドキュメントの指定パスへの書込み方法の代表的なものとして、setメソッドがあります。
参考Firebase firestore:Writing Data

これは、従来のデータベースのinsertに近いイメージですが、mergeオプションをセットすることでupsertのような、update、もしなければinsertといった挙動をさせることができます。

よって、Firestoreドキュメントの指定パスへの書込み方法の分類はmergeオプションを区別すると
set without mergeset with mergeupdateの3種類となります。
(今回はmergeFieldsオプションについてはmergeとして扱います)

この3つには下記のような違いがあります。

  • set without merge: ドキュメントそのものをupdate、もし存在しないようならcreate
  • set with merge: ドキュメントのフィールドのupdate、 もし存在しないようならcreate
  • update: ドキュメントのフィールドのupdate、 もし存在しないようなら失敗する
サンプルコード.swift
//すべて同期的に動作するものと仮定したコードです

let firestore = Firestore.firestore()

//新規documentIDでReferensePathを作成
let userRef = firestore.collection("users").document()
let data1 = ["name":"userA"]

//ドキュメントが存在しないため失敗する
userRef.updateData(data1)

//ドキュメントが存在しないのでcreateされる
userRef.setData(data1, merge: true)
/*
user > {documentID} > ["name":"userA"]
*/

//ドキュメントそのものがupdateされるためnameフィールドは消える
let data2 = ["age": 21]
userRef.setData(data2)
/*
user > {documentID} > ["age": 21]
*/

//ドキュメントが存在するためフィールドがupdateされる。ageフィールドは消えない
//userRef.updateData(data1, merge: true)と同じ挙動
userRef.setData(data1, merge: true)
/*
user > {documentID} > ["name":"userA", "age": 21]
*/

set without mergeはすでに存在するドキュメントを無視する

まだ登録されてないuserIDのprofileだけ登録したい等の目的でcreateを目的としたsetメソッドを使用して書き込み処理を行なっても、すでに存在するdocumentPathに対してはupdateの挙動となってしまいます。
なので、

  • すでに存在するドキュメントの上書き作成を防ぎたい
  • 通常の部分的なフィールドの更新は許可したい

といった意図をセキュリティルールで実現するには、update権限のルールを工夫する必要があります。

1度しかデータを作成できないようにする

方法としては、初回作成時にしか書き込まないcreatedAtのようなフィールドを利用します。
具体的には

  • データ作成時は必ずcreatedAtを書き込むこと
  • 下記のようなルールでcreatedAtの更新のみを禁止すること

によって、すでに存在するドキュメントにcreateしようとした処理を失敗させることができます。

match /users/{userID} {
      allow read: if request.auth.uid != null;
      allow create: if request.auth.uid == userID
                    && request.resource.data.createdAt != null
      allow update: if request.auth.uid == userID
                    && request.resource.data.createdAt == resource.data.createdAt;

ちなみに
request.resource.data.createdAt == resource.data.createdAt
のようなルールをallow文に記述することで、フィールドの変更を制限することができます。

20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16