为多对多关系实施Firestore安全规则

时间:2020-05-10 17:03:53

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

我正在努力使用Firestore安全规则来确保多对多关系。 我有以下收藏:

Key:
documentId: [field: value, ...]

groups {
    group1: [name: Group1]
    group2: [name: Group2]
}

users {
    bobUser:   [name: Bob]
    aliceUser: [name: Alice]
    fredUser:  [name: Fred]
}

// Contains data specific to a user in a particular group.
// Specifically the user's role
userGroups {
    userGroup1: [userId: bobUser,   groupId: group1, role: "admin"]
    userGroup2: [userId: aliceUser, groupId: group1, role: "member"]
    userGroup3: [userId: fredUser,  groupId: group2, role: "admin"]
}

我该如何构建Firestore安全规则,以便:

  • 具有role:"admin"的用户可以阅读另一个用户的文档(如果他们都在同一组中)

因此,在上面的示例中,Bob可以读取Alice的用户文档,因为他具有"admin"角色,但是Fred不能读取,因为他是另一个组的管理员。 或换一种说法:

如果bobUser发出以下请求,则它应通过安全规则:

db.collection("users").doc("aliceUser");

因为鲍勃(Bob)与爱丽丝(Alice)在同一组中担任管理员角色

相反,如果登录fredUser,则以下请求将失败:

db.collection("users").doc("aliceUser");

Fred是管理员用户,但不在同一组中,因此该规则将阻止该请求。

在安全规则中,我认为我需要分为几个阶段:

  • 查询userGroups以查找请求userId为角色的所有groupId:“ admin”
  • 查询userGroups以查找请求的用户所在的所有groupIds
  • 如果两个组中的groupId均匹配,则允许写入

但我很难将此逻辑纳入规则。安全规则似乎无法像这样过滤。任何帮助都会很棒!

1 个答案:

答案 0 :(得分:2)

为了解决此问题,您需要记住您不能将关系数据库模式转移到非关系数据库。使用Firestore时,应首先询问“应该进行哪些查询?”。然后基于此结构化数据。建立安全规则自然而然。

我写了一篇关于您的用例的博客文章:"How to build a team-based user management system with Firebase",因此,如果不清楚以下答案中的任何内容,请先去那里看看是否有帮助。

以您为例,您可能需要以下查询:

  1. 获取组中的所有用户(假设当前用户是该组的成员并具有正确的权限)。
  2. 获取用户的所有组(假设您仅查询当前已认证用户的组)。

正如您所注意到的,通过Firestore和安全规则很难处理多对多关系,因为您需要提出其他请求才能加入数据集。为避免这种情况,我建议将userGroups集合重命名为memberships,并将其变成groups集合中每个文档的子集合。所以您的新结构看起来像

- collection "groups", a doc contains:
  - field "name"
  - collection "memberships", a doc contains:
    - field "name"
    - field "role"
    - field "user" → references doc from "users"
- collection "users", a doc contains:
  - field "name"

通过这种方式,您可以通过查询子集合“成员资格”(例如collection("groups").doc("your-group-id").collection("memberships").get())来轻松解决第一个查询“获取组的所有用户”。

现在,为了确保这一点,您可以在“安全规则”中编写一个辅助函数:

function hasAccessToGroup(groupId, role) {
  return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}

给定一个groupId和一个角色,您可以使用它仅允许作为成员并具有特定角色的用户访问组中的数据和子集合。为了保护组中的成员资格集合,可能如下所示:

rules_version = '2'
service cloud.firestore {
  match /databases/{database}/documents {
    function hasAccessToGroup(groupId, role) {
      return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
    }
    match /groups/{groupId} {
      // Allow users with the role "user" access to read the group doc.
      allow get: if
        request.auth != null &&
        hasAccessToGroup(groupId, "user")

      // Allow users with the role "admin" access to read all subcollections, including "memberships".
      match /{subcollection}/{document=**} {
        allow read: if
          request.auth != null &&
          hasAccessToGroup(teamId, "admin")
      }
    }
  }
}

现在只剩下第二个查询“获取用户的所有组”。这可以通过集合组索引来实现,该集合组索引允许查询具有相同名称的所有子集合。您想create one来获取会员资格集合。给定特定用户,您可以轻松地使用collectionGroup("memberships").where("user", "==", currentUserRef).get()查询其所有组。

为了确保这一点,您需要设置一个规则,仅在查询的用户引用等于当前经过身份验证的用户时才允许此类请求:

function isReferenceTo(field, path) {
  return path('/databases/(default)/documents' + path) == field
}

match /{document=**}/memberships/{userId} {
  allow read: if
    request.auth != null &&
    isReferenceTo(resource.data.user, "/users/" + request.auth.uid)
}

最后要讨论的一件事是如何使成员资格集合中的数据与它引用的用户文档中的数据保持最新。答案是云功能。每次用户文档更改时,您都会查询其所有成员身份并更新数据。

如您所见,回答您最初的问题如何构造Firestore规则,以使具有正确权限的用户可以在另一个用户的文档位于同一组中的情况下阅读他们的文档,这采用了不同的方法。但是,在重组数据之后,您的安全规则将更易于阅读。

我希望这会有所帮助。干杯!