【问题标题】:Firestore security rule match path to a field in dictionaryFirestore 安全规则匹配字典中字段的路径
【发布时间】:2020-03-31 21:31:08
【问题描述】:

假设我在用户对象下方有这个 Firestore,其中包含字段名称、地址和汽车(注意只有用户是一个集合)。

user {
      name: "John Smith"
      address: '123 Firebase Road, Firestore CA, 10000"
      cars: {
               asfdfsd811r9UAdfasdf1: {
                       name: "Ford Explorer"
                       carSold: false,
                       salesComment: "This is the best SUV in the world"
               },
               12342342ADSfas! :{
                        name:" Testla Modal X"
                        carPrice:false,
                       salesComment: "This is the best electric car in the world"
               }
      }
}

我想设置一个安全规则来强制客户端库只能编辑salesComment,但是这个用户集合对象中没有其他内容,我该怎么做?我设置了一个如下所示的匹配路径,但它不起作用:(。你可以设置字段字典的匹配路径,比如在这种情况下,汽车吗?匹配和 variableId 模式是否仅适用于集合。

service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userId} {
              match /cars/{carId}/salesComment {
                    allow write: if request.auth.uid == userId;
              }
               allow read: if request.auth.uid == userId;
        }

【问题讨论】:

  • 您不能在匹配路径中使用文档字段的名称。您必须在规则中编写代码以检查字段数据是否仅按照您想要的方式进行修改。
  • 嗨,Doug,您的 Firebase 视频的忠实粉丝(观看了所有关于 Promise 的视频超过 3 次)。回复:不能匹配路径中文档字段的用户名,那么甚至可以检查用户是否正在编辑文档字段字典中的字典的特定键?那么在这种情况下,是否不能只允许用户编辑文档字段汽车字典中的字典的 salesComment 键?所以你甚至不能使用像 match /user/{restOfPath=**} { restOfPath.matches('cars/.+/salesComment/.+') 这样的正则表达式,对吧?
  • 挑战在于有一个未知的密钥 - carId - 这是未知的,并且无法遍历 Firestore(或 RTDB)安全规则中的字典。只有用户是集合。为了简单起见,我只是编造了这个例子,但似乎无法做到我想要实现的目标。
  • 就像我可以做的 (request.resource.data.diff(resource.data).changedKeys().hasOnly(["cars"]) 并允许用户编辑汽车字段中的所有内容用户集合。但这不是我想要的。我想强制用户只能编辑该汽车字段的特定子字段。
  • @DougStevenson 请让我知道这是否可能。如果不可能,也告诉我。现在我让用户只编辑集合的整个字段。就我的项目而言,这种特殊的安全规则限制非常小。但我可以将此作为 Firebase 的一项功能,因为您可以使用 user/cars/{carId}/salesComment 等路径更新字段但您无法在安全规则中匹配它,这很奇怪。

标签: firebase google-cloud-firestore firebase-security


【解决方案1】:

解决方法:自定义函数

您不能对内部字段使用匹配,而是必须使用 rules.Listrules.Maprules.Set 对象。

请务必注意,规则是​​静态的,无法遍历列表(例如使用 forEachmap 等)。这可以通过使用someList.size() <= position 在执行元素比较之前检查列表是否足够长来克服。不幸的是,这必须是硬编码的,如下所示。

这些规则的一个目标是它们应该能够与同一文档上的其他规则结合使用。即“汽车”地图应该受到限制,但您仍然应该能够更新“名称”和“地址”字段。

在本节中,变量将非常冗长以便于理解(例如包括类型信息)。重命名它们以适合您的风格。

免责声明:虽然这第一组规则有效,但它很笨拙且过于具体 - 不建议用于生产环境。

service cloud.firestore {
  match /databases/{database}/documents {

    // assert no changes or that only "salesComment" was changed
    function isCarEditAllowed(afterCarMap, beforeCarMap) {
      return afterCarMap.diff(beforeCarMap).affectedKeys().size() == 0
          || afterCarMap.diff(beforeCarMap).affectedKeys().hasOnly(["salesComment"]);
    }

    // assert that if this car exists that it has allowed changes
    function isCarAtPosValid(afterCarsList, beforeCarsList, position) {
      return afterCarsList.size() <= position // returns true when car doesn't exist
          || isCarEditAllowed(afterCarsList[position], beforeCarsList[position])
    }

    function areCarEditsAllowed(afterDataMap, beforeDataMap) {
      return afterDataMap.get("cars", false) != false // cars field exists after
          && beforeDataMap.get("cars", false) != false // cars field exists before
          && afterDataMap.cars.size() == beforeDataMap.cars.size() // cars field is same length
          && isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 0)
          && isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 1)
          && isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 2)
          && isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 3)
          && isCarAtPosValid(afterDataMap.cars, beforeDataMap.cars, 4)
    }

    match /carUsers/{userId} {
      allow read: if request.auth.uid == userId;

      allow write: if request.auth.uid == userId
                   && areCarEditsAllowed(request.resource.data, resource.data)
    }
  }
}

既然上述规则有效,可以通过将步骤抽象为一组可重用的自定义函数来改进它们。

service cloud.firestore {
  match /databases/{database}/documents {

    /* Custom Functions: Restrict map changes */

    function mapHasAllowedChanges(afterMap, beforeMap, setOfWhitelistedKeys) {
      return afterMap.diff(beforeMap).affectedKeys().size() == 0 // no changes
          || setOfWhitelistedKeys.hasAll(afterMap.diff(beforeMap).affectedKeys()) // only named keys may be changed
    }

    function mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, position) {
      return afterList.size() <= position // returns true when element doesn't exist
          || mapHasAllowedChanges(afterList[position], beforeList[position], setOfWhitelistedKeys)
    }

    function listOfMapsHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys) {
      return mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 0)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 1)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 2)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 3)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 4)
    }

    function largeListOfMapsHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys) {
      return mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 0)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 1)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 2)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 3)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 4)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 5)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 6)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 7)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 8)
          && mapInListHasAllowedChanges(afterList, beforeList, setOfWhitelistedKeys, 9)
    }

    function namedListWithSameSizeExists(listPath) {
      return request.resource.data.get(listPath, false) != false
          && resource.data.get(listPath, false) != false
          && request.resource.data.get(listPath, {}).size() == resource.data.get(listPath, {}).size()
    }

    function namedListOfMapsWithSameSizeExistsWithAllowedChanges(listPath, setOfWhitelistedKeys) {
      return namedListWithSameSizeExists(listPath)
          && listOfMapsHasAllowedChanges(request.resource.data.get(listPath, {}), resource.data.get(listPath, {}), setOfWhitelistedKeys)
    }

    /* Rules */

    match /carUsers/{userId} {
      allow read: if request.auth.uid == userId;

      allow write: if request.auth.uid == userId
                   && namedListOfMapsWithSameSizeExistsWithAllowedChanges("cars", ["salesComment"].toSet())
    }
  }
}

注意:上述规则并不断言白名单键未被删除。为确保更改后列出的键存在,您需要将 mapHasAllowedChanges 函数替换为:

function mapHasAllowedChanges(afterMap, beforeMap, setOfWhitelistedKeys) {
      return afterMap.diff(beforeMap).affectedKeys().size() == 0 // no changes
          || (setOfWhitelistedKeys.hasAll(afterMap.diff(beforeMap).affectedKeys()) // only named keys may be changed
          && afterMap.keys().toSet().hasAll(setOfWhitelistedKeys)) // all named keys must exist
    }

推荐的方法:移动到汽车到子集合

上述规则相当复杂,如果您将汽车移至自己的收藏并使用rules.Map#diff,则可以简化。

只有在用户拥有该汽车文档并且仅修改salesComment 键(修改 = 添加/更改/删除)时,以下代码才允许进行写入。

service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userId} {
      allow read, write: if request.auth.uid == userId;

      match /cars/{carId} {
        allow read: if request.auth.uid == userId; // Firestore rules don't cascade to subcollections, so this is also needed

        allow write: if request.auth.uid == userId
                     && request.resource.data.diff(resource.data).affectedKeys().hasOnly(["salesComment"]);
      }
    }
  }
}

如果您要求salesComment 在写入后必须存在(允许添加/更改 - 但不能删除),您还可以使用k in x 确保它仍然存在。

service cloud.firestore {
  match /databases/{database}/documents {
    match /user/{userId} {
      allow read, write: if request.auth.uid == userId;

      match /cars/{carId} {
        allow read: if request.auth.uid == userId; // Firestore rules don't cascade to subcollections, so this is also needed

        allow write: if request.auth.uid == userId
                     && "salesComment" in request.resource.data
                     && request.resource.data.diff(resource.data).affectedKeys().hasOnly(["salesComment"]);
      }
    }
  }
}

【讨论】:

  • 第一个不起作用,因为只有用户是一个集合。汽车不是收藏品。 Car 是用户集合的字典的键。第二个不行吧?当用户编辑 salesComment 字段时, request.resource.data.diff(resource.data).affectedKeys() 不会返回“cars”,而不是“salesComment”?因为 salesComment 在字典的字典中。
  • 我想我必须获取所有键,然后在 0、1、2 等处查找索引。这是唯一可悲的方法。
  • @coolcool1994 我发现我最初误解了这个问题。我做了一些研究,并将我的发现添加到我的答案中。
  • 我做了类似的事情。我给了你赏金。虽然我还没有测试过。一旦我检查它是否有效,我会接受它。
  • 我确认这行得通。唯一需要注意的是,您需要拥有固定数量的汽车。如果您有无限数量的汽车,则无法强制编辑特定字段-但我想这是将其包含在文档中的一个要点,否则它应该是另一个集合。在每个级别使用差异并检查 hasOnly 以强制执行规则。我在我的项目中使用它。
猜你喜欢
  • 1970-01-01
  • 2020-02-13
  • 2021-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多