Firestore安全规则匹配字典中字段的路径

时间:2020-03-31 21:31:08

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

假设我在用户对象下面有此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,但是此用户集合对象中没有其他内容,我该怎么做?我设置了一个如下所示的匹配路径,但::不起作用。您可以设置字段字典的匹配路径,例如在这种情况下,是car吗?match和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;
        }

1 个答案:

答案 0 :(得分:3)

解决方法:自定义功能

您不能使用内部字段匹配,而必须使用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键(modify =添加/更改/删除)的情况下才允许进行写操作。

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"]);
      }
    }
  }
}