Firestore安全规则:批处理时出现“PERMISSION DENIED”错误

5

考虑这个简单的Firestore数据库结构:

1:Firestore 结构

cities/
    city1/
        name:Beijing
        likes: 0
        dislikes: 0
        ... more fields 
    city2/
        name: New York
        likes: 21
        dislikes: 1
        ... more fields 

为保护数据库免受意外操作的影响,我添加了以下Firestore安全规则:

2个Firestore安全规则:

// Allow read/write access to all users under any conditions
service cloud.firestore {
  match /databases/{database}/documents {    
    match /cities/{cityId} {
      allow read: if request.auth.uid != null;
      allow update: if request.auth.uid != null && 
request.resource.data.keys().hasOnly(["likes", "dislikes"]); 
    }
  }
}

这基本上允许我强制执行以下要求:

  • 已认证的用户可以读取城市数据。
  • 已认证的用户可以更新城市的“喜欢”和“不喜欢”字段。

问题

当我尝试使用批量写入时,为什么上述设置会给我一个Exception: PERMISSION_DENIED: Missing or insufficient permissions.,但如果我单独编写更改,则成功了呢?

例如下面的代码(使用Batch write)失败:PERMISSION_DENIED: Missing or insufficient permissions。

fun rate(likes: List<String>, dislikes: List<String>, done: (Boolean) -> Unit) {

    db.runBatch { batch ->
        likes.map { cityCollection.document(it) }.forEach { doc ->
            batch.update(doc,"likes", FieldValue.increment(1))
        }
        dislikes.map { cityCollection.document(it) }.forEach { doc ->
            batch.update(doc,"dislikes", FieldValue.increment(1))
        }
    }.addOnCompleteListener { task ->
        task.exception() // Exception: PERMISSION_DENIED: Missing or insufficient permissions.
        done(task.isSuccessful) // false
    }
}

如果不进行批处理,代码可以正常工作。例如,以下代码可以完美运行:

override fun rate(likes: List<String>, dislikes: List<String>, done: (Boolean) -> Unit) {

    val tasks = likes.map { cityCollection.document(it) }.map { doc ->
        doc.update("likes", FieldValue.increment(1))
    }.union(dislikes.map { cityCollection.document(it) }.map { doc ->
        doc.update("dislikes", FieldValue.increment(1))
    })

    Tasks.whenAllComplete(tasks).addOnCompleteListener { task ->
        done(task.isSuccessful) // true
    }
}

在批量写入/事务方面,是否有什么特殊的需要我在安全规则方面了解的?我希望这些更新能够原子执行,因此一开始尝试使用批量写入。然而,我似乎无法将其与我的安全规则结合使用。

2个回答

4
你需要知道的是,request.resource.data.keys() 包含现有文档中所有的键,而不仅仅是正在更新的键。如果允许写操作完成,request.resource.data 表示文档的最终状态。因此,如果你要更新已经包含其他字段的文档,你的规则将始终拒绝访问,因为 hasOnly 将返回 false。
由于在两种情况下都没有显示现有文档的内容,所以很难准确地说这是代码出了什么问题。但几乎可以肯定的是,这就是问题所在。无论更新是来自单个文档写入、批量更新还是事务,它们都受相同规则的限制。
(过去可以使用一个名为 writeFields 的属性来找出正在更新的字段,但这已被弃用 - 不要使用它。)
如果你想执行每个字段的限制,实际上要比你现在编写的要复杂得多。你必须检查特定字段是否已被修改,同时还要检查没有其他字段被更改。详见这个问题:

Cloud Firestore安全规则 - 仅允许写入文档中特定的键


谢谢!我已经在考虑将“likes”和“dislikes”移动到它们自己的子集合中(这可能仍然更容易)。 - Entreco
刚刚花了几个小时来解决这个问题。一直在模拟对不存在的UID进行“更新”,结果得到了错误的正面反馈;显然,测试是针对一个空的resource运行的,所以如果我的请求数据匹配任何hasOnly字段,它总是会返回ok。有没有办法模拟已经存在的数据??在我看来,确实需要一种只针对正在更新的字段进行测试的方法,否则通过管理员界面(安全规则不适用的地方)添加任意字段都将破坏用户界面,除非也更新安全规则。 - som
1
将来会有更简单的方法,但今天你必须检查所有已知的字段。 - Doug Stevenson
现在就可以了吗? - som
严格来说,“现在”就是此时此刻。;-) 但是如果你问什么时候会有变化,我不知道未来。我只知道这已经被讨论过了。 - Doug Stevenson
好的,没问题。如果它能让你感受到当前设置所需的规则集有多么复杂 - 我之前并没有意识到有一个1000倍的表达式限制,直到我在试图保护(并严格类型化)一个由19个用户定义和触发器附加数据字段组成的相当简单的文档时才遇到了这个限制。 - som

0

您的文档request.resource.data对象可能会有更多的字段,而不仅仅是“likes”和“dislikes”,这将导致规则失败。检查哪些字段已更改的新规范方法是通过将文档与现有文档进行差异化比较(这替换了“writeFields”)。我已经编辑了您的规则以显示此内容:

service cloud.firestore {
  match /databases/{database}/documents {    
    match /cities/{cityId} {
      function onlyLikedFieldUpdated() {
        return request.resource.data.diff(resource.data).affectedKeys().hasOnly(["likes", "dislikes"]);
      }
      allow read: if request.auth.uid != null;
      allow update: if request.auth.uid != null && onlyLikedFieldUpdated();
    }
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接