Firestore 安全规则 - 如何检查某个字段是不是被修改?

Posted

技术标签:

【中文标题】Firestore 安全规则 - 如何检查某个字段是不是被修改?【英文标题】:Firestore Security Rules - How can I check that a field is/isn't being modified?Firestore 安全规则 - 如何检查某个字段是否被修改? 【发布时间】:2018-06-18 23:46:38 【问题描述】:

对于我的一生,我无法理解为什么以下内容会导致false 允许写入。假设我的 users 集合一开始是空的,我正在从我的 Angular 前端编写以下形式的文档:


  displayName: 'FooBar',
  email: 'foo.bar@example.com'

我目前的安全规则:

service cloud.firestore 
  match /databases/database/documents 
    match /users/userId 
      function isAdmin() 
        return resource.data.role == 'ADMIN';
      

      function isEditingRole() 
        return request.resource.data.role != null;
      

      function isEditingOwnRole() 
        return isOwnDocument() && isEditingRole();
      

      function isOwnDocument() 
        return request.auth.uid == userId;
      

      allow read: if isOwnDocument() || isAdmin();
      allow write: if !isEditingOwnRole() && (isOwnDocument() || isAdmin());
    
  

一般来说,我不希望任何用户能够编辑自己的角色。普通用户可以编辑自己的文档,管理员可以编辑任何人的文档。

false 存根isEditingRole() 给出了预期的结果,因此我将其范围缩小到该表达式。

写入总是返回错误,我无法确定原因。任何想法或修复都会有所帮助!

编辑 1

我尝试过的事情:

function isEditingRole() 
  return request.resource.data.keys().hasAny(['role']);

function isEditingRole() 
  return 'role' in request.resource.data;

function isEditingRole() 
  return 'role' in request.resource.data.keys();

编辑 2

请注意,管理员最终会为用户设置角色,因此角色最终可能存在于文档中。这意味着,根据下面的Firestore docs,该请求将有一个role 键,即使它不在原始请求中。

资源中存在的请求中未提供的字段将添加到request.resource.data。规则可以通过比较 request.resource.data.fooresource.data.foo 来测试字段是否被修改,因为知道 resource 中的每个字段也会出现在 request.resource 中,即使它没有在写入请求中提交。

据此,我认为排除了“编辑1”中的三个选项。我确实尝试了request.resource.data.role != resource.data.role 的建议,但这也不起作用……我不知所措,开始怀疑 Firestore 中是否真的存在错误。

【问题讨论】:

【参考方案1】:

如果您创建一个自定义函数来检查更新,您的规则将更具可读性和可维护性。例如:

service cloud.firestore 
  match /databases/database/documents 
    function isUpdatingField(fieldName) 
      return (!(fieldName in resource.data) && fieldName in request.resource.data) || resource.data[fieldName] != request.resource.data[fieldName];
    

    match /users/userId 
      // Read rules here ...
      allow write: if !isUpdatingField("role") && !isUpdatingField("adminOnlyAttribute");
    
  

【讨论】:

不幸的是,我已经根据文档尝试了所有“应该”工作的方式,这就是其中之一。我将更新我的帖子以包含我尝试过的内容。 @menehune23 我认为 keys().hasAny() 正在工作。如果你说它不是,我们绝对应该在 Google 小组中提出它应该是的! 是的,我仍然很困惑。我实际上发现使用inhasAny() 似乎都按预期运行,但由于某种原因,role 字段被添加到我不希望它的请求中(即即使@987654325 @ 在资源中不存在)。可能是缓存问题?正如我过去将它添加到文档中一样,但清除了集合以重新开始。 当我更清楚时我会发布更新,但我开始取得一些进展。 它不起作用,很遗憾谷歌误导了我们【参考方案2】:

所以最后,我似乎假设resource.data.nonExistentField == null 会返回false,而实际上它返回Error(根据this 和我的测试)。所以我最初的解决方案可能已经遇到了。这令人费解,因为根据the docs,相反的情况应该起作用,但也许文档指的是“不存在”的值,而不是键——一个微妙的区别。

我仍然没有 100% 清楚,但这是我最终得到的结果:

function isAddingRole() 
  return !('role' in resource.data) && 'role' in request.resource.data;


function isChangingRole() 
  return 'role' in resource.data && 'role' in request.resource.data && resource.data.role != request.resource.data.role;


function isEditingRole() 
  return isAddingRole() || isChangingRole();

另一件让我感到困惑的事情是,根据文档,我不需要 isChangingRole() 中的 && 'role' in request.resource.data 部分,因为它应该由 Firestore 自动插入。尽管情况似乎并非如此,但删除它会导致我的写入因权限问题而失败。

可以通过将写入分成createupdatedelete 部分来澄清/改进,而不仅仅是allow write: if !isEditingOwnRole() && (isOwnDocument() || isAdmin());

【讨论】:

【参考方案3】:

我使用writeFields 解决了这个问题。请试试这个规则。

allow write: if !('role' in request.writeFields);

就我而言,我使用list 来限制更新字段。它也有效。

allow update: if !(['leader', '_created'] in request.writeFields);

【讨论】:

您的in 规则似乎总是评估为真。 in 运算符仅检查给定列表是否具有特定值(不适用于列表)。为了做你想做的事,试试这个:!(request.writeFields.hasAny(['leader', '_created'])); request.resource.keys.hasAny() 方法最适合不使用 SDK 并坚持使用模拟器的人。 writeFields 不再出现在文档中,显然已被弃用。 ***.com/a/52192476/4458849【参考方案4】:

Tom Bailey (https://***.com/a/48177722/5727205) 的解决方案看起来很有希望。

但在我的情况下,我需要防止字段被编辑,并且可能会出现该字段根本不存在于现有数据中的情况。因此,我添加了一个检查该字段是否存在。

此解决方案会检查两项检查:

    如果该字段不在请求中且不在现有数据中(等于字段未修改) 或请求与现有数据相同(等于字段未修改)
 function isNotUpdatingField(fieldName) 
   return
     ( !(fieldName in request.resource.data) && !(fieldName in resource.data) ) || 
     request.resource.data[fieldName] == resource.data[fieldName];

【讨论】:

【参考方案5】:

通过这个单一的功能,您可以检查一个字段是否正在/没有被创建/修改。

function incomingDataHasFields(fields) 
    return ((
        request.writeFields == null
        && request.resource.data.keys().hasAll(fields)
    ) || (
        request.writeFields != null
        && request.writeFields.hasAll(fields)
  ));

用法:

match /xxx/xxx     
    allow create:
        if incomingDataHasFields(['foo'])              // allow creating a document that contains 'foo' field
           && !incomingDataHasFields(['bar', 'baz']);  // but don't allow 'bar' and 'baz' fields to be created

【讨论】:

【参考方案6】:

这是一个将所有这些都考虑在内的函数:

不会触发诸如“对象上的属性名称未定义”之类的安全规则错误。 适用于set()update() 在使用firebase.firestore.FieldValue.delete() 明确尝试删除字段时有效 可在创建和更新文档时使用(updatecreate)。
function fieldNotWrittenByUser(field) 
  return (
    (
      (field in request.resource.data)
      && (resource != null && field in resource.data)
      && request.resource.data[field] == resource.data[field]
    )
    || (
      !(field in request.resource.data) && (resource == null || !(field in resource.data))
    )
  )

说明

请记住,request.resource.data 表示 成功写入操作后的资源,即“未来”文档。

如果该字段出现在未来的文档中,它必须与现有文档中的值相同。

或者,如果将来的文档中不存在该字段,则仅当资源尚不存在或现有文档也缺少该字段时才允许该操作。

来源:https://www.sentinelstand.com/article/firestore-security-rules-examples

【讨论】:

“(记住 request.resource.data 代表成功写入操作后的资源)”对我来说值得点赞:)【参考方案7】:

由于文档中对 writeFields 的引用已经消失,我不得不想出新的方法来做我们可以用 writeFields 做的事情。

function isSameProperty(request, resource, key) 
    return request.resource.data[key] == resource.data[key]


match /myCollection/id 
    // before version !request.writeFields.hasAny(['property1','property2','property3', 'property4']);
  allow update: isSameProperty(request, resource, 'property1')
    && isSameProperty(request, resource, 'property2')
    && isSameProperty(request, resource, 'property3')
    && isSameProperty(request, resource, 'property4')
  

【讨论】:

我不得不做类似的事情。如果我们以后在文档中添加更多字段也不好,我们会很高兴有一个像“[资源没有修改,除了这个字段]然后OK”这样的功能,而不是手动为所有字段重复“isSameProperty”......还需要更新此规则...如果存在我错过的此类功能,很想知道!或者如果有更好的方法来解决这个问题。【参考方案8】:

这似乎有点过头了,但是对于更新文档,您可能有其他非用户生成的字段,例如。角色,创建等。您需要可以测试这些字段不会更改的功能。因此满足这三个 FN。

function hasOnlyFields(fields) 
  if request.resource.data.keys().hasOnly(fields) 

function hasNotChanged(fields) 
  return (fields.size() < 1 || equals(fields[0]))
    && (fields.size() < 2 || equals(fields[1]))
    && (fields.size() < 3 || equals(fields[2]))
    && (fields.size() < 4 || equals(fields[3]))
    && (fields.size() < 5 || equals(fields[4]))
    && (fields.size() < 6 || equals(fields[5]))
    && (fields.size() < 7 || equals(fields[6]))
    && (fields.size() < 8 || equals(fields[7]))
    && (fields.size() < 9 || equals(fields[8]))

function equals(field) 
  return field in request.resource.data && field in resource.data && request.resource.data[field] == request.resource.data[field]

所以在更新用户文档时,用户只能更新他们的姓名、年龄和地址,而不能更新角色和电子邮件:

allow update: if hasOnlyFields(['name', 'age', 'address']) && hasNotChanged(['email', 'roles'])

注意 hasNotChanged 最多可以检查 9 个字段。此外,这些不是您想要做的唯一检查。您还需要检查文档的类型和所有权。

【讨论】:

【参考方案9】:

发现这条规则运作良好:

function propChanged(key) 
  // Prop changed if key in req but not res, or if key req and res have same value
  return (
    (key in request.resource.data) && !(key in resource.data)
  ) || (
    (key in request.resource.data) && request.resource.data[key] != resource.data[key]
  );

【讨论】:

以上是关于Firestore 安全规则 - 如何检查某个字段是不是被修改?的主要内容,如果未能解决你的问题,请参考以下文章

包含数组的查询的 Firestore 安全规则

即使firestore安全规则中的文档字段不可用,也如何允许访问?

有没有办法使用Firestore安全规则使用FieldValue.increment()更新文档字段?

在 Cloud Firestore 规则中 - 如何检查密钥是不是为空

Firestore 安全规则:如果时间戳 (FieldValue.serverTimestamp) 等于现在

Firebase Firestore 安全规则 |如果字段值与 auth uid 相同,则允许读取