使用Firestore安全规则限制写在嵌套对象上的字段

时间:2020-05-23 20:33:39

标签: google-cloud-firestore firebase-security

tl; dr::我认为Set需要一种获取元素(set.toList()[0])的方法,但也许我缺少了一些东西!


你好!我正在使用Firestore开发带有大量小对象(信用卡交易)的预算应用程序。为了限制读取次数,将每个事务存储为单独的文档是没有意义的,因为用户可能一次想要约数百个事务。

相反,我有一个容器来容纳许多看起来像这样的交易:

/user/{user_id}/transactions/{container_id}

container: {
  transactions: {
    transaction_id_1: {
      amount: 8.25,
      note: 'chipotle lunch'
    },
    transaction_id_2: {
      amount: 12.01
    }
  }
}

这很好用,但是我认为安全规则不能用于写操作。我想允许用户修改某些字段(note),但不能修改其他字段(amount)。如果每笔交易都是一个文档,我们可以使用MapDiff进行此操作,但是嵌套会增加难度。

由于我们无法编写循环,因此如果每次写入将自己限制为一个更新的事务,那么使用嵌套的MapDiff这样的代码应该完全有可能:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents/{document=**} {

    function allowTransactionUpdate() {
      let transactionId = <transaction ID of the single transaction being updated>;

      // Limit fields updated for container.
      return request.resource.data.diff(resource.data).changedKeys()
        .hasOnly(['transactions']) &&

      // Make sure only one transaction changed.
      request.resource.data.transactions.diff(resource.data.transactions)
        .changedKeys().hasOnly([transactionId]) &&

      // Verify the transaction already exists.
      transactionId in resource.data.transactions &&

      // Only allow certain fields to be updated on that transaction.
      request.resource.data.transactions[transactionId]
        .diff(resource.data.transactions[transactionId]).affectedKeys()
        .hasOnly(['note']);
    }

    match /transactions/{transMonthId} {
      allow update: if allowTransactionWrite();
    }

    allow read, write: if false;
  }
}

如果我们可以使用MapDiff来获取container.transactions地图中已更改的交易,这将非常有用...

let transactionId = request.resource.data.transactions
  .diff(resource.data.transactions).changedKeys()[0];

缺少的关键部分是最后一位:[0]。当前,Sets无法获得元素,这意味着将某些内容转换为Set(以及使用MapDiff进行的任何操作)是一个死胡同:您永远无法真正知道Set中的值。只能与其他集合进行比较。

否则...我错过了什么吗?还有另一种方法来限制嵌套更新上的字段吗?


其他选项是:

  • 使用自定义后端来执行此写操作,这是可行的,但令人沮丧,因为Firestore的一大优势是最小化了后端+强制执行了安全规则。
  • 将用户可编辑的属性放在一个容器文档中,将不可编辑的属性放在另一个容器文档中,但这会使读取次数翻倍,并给客户端订阅增加烦人的复杂性。
  • 接受这是不可能的,并且每次事务使用一个文档,这将导致读取增加100倍。 ;)

2 个答案:

答案 0 :(得分:2)

您没有丢失任何东西。安全规则无法实现您要尝试执行的操作。

如果您打算收集数据项,并且要引用这些数据项并使用安全规则进行保护,则它们应该是集合或子集合中的单个文档。建议不要将它们全部塞在一个文档中,也不能扩展。如果您这样做是为了节省文档读取的内容,那么您很快就会发现,在涉及安全规则和管理这些单个项目时,这种“优化”实际上不是非常有用。将数据项作为单个文档保护比在单个文档中进行管理要容易得多,更直接。

如果您真的必须将所有内容存储在一起,建议您通过一些后端来限制写访问权限,在这里您可以编写自定义逻辑,并让您的客户端在需要执行写操作时调用后端。请记住,这是不可扩展的,并且您可能遇到的最大文档大小为1MB,这比开始时要解决的问题要昂贵得多。

答案 1 :(得分:0)

对于正在寻找嵌套对象和MapDiff示例的其他人

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    function affectedKeys(keys){
      return request.resource.data.diff(resource.data).affectedKeys().hasOnly(keys)
    }

    function affectedKeysObj(obj1Key, obj2Key, keys){
      return request.resource.data[obj1Key].diff(resource.data[obj2Key]).affectedKeys().hasOnly(keys)
    }

    match /{document=**} {
      allow read, write: if false;
    }
    match /users/{uid}{
      allow get: if request.auth.uid == uid;
      allow update: if request.auth.uid == uid 
        && ! affectedKeys(["meta"]) 
        && affectedKeys(["userData"]) 
        && affectedKeysObj("userData", "userData", ["bio", "displayName"]);
    }
  }
}

在这种情况下,我希望用户能够在["bio", "displayName"]地图内编辑userData,但是我也想禁止编辑meta地图。

无论如何,道格·史蒂文森斯(Doug Stevensons)是对的,我只是补充道,这就是我将MapDiff与嵌套对象一起使用的方式。