【问题标题】: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 中是否真的存在错误。

【问题讨论】:

    标签: firebase firebase-security google-cloud-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 不再出现在文档中,显然已被弃用。 stackoverflow.com/a/52192476/4458849
      【解决方案4】:

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

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

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

      1. 如果该字段不在请求中且不在现有数据中(等于字段未修改)
      2. 或请求与现有数据相同(等于字段未修改)
       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]
              );
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2020-01-14
              • 1970-01-01
              • 2019-04-26
              • 2020-02-13
              • 2020-09-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多