MongoDB:如何基于应用程序访问模式设计架构?

时间:2019-07-12 14:04:39

标签: mongodb database-design nosql mongoengine

作为来自DynamoDB的人,对MongoDB模式进行建模以使其真正适合我的应用程序有点令人困惑,特别是因为它具有引用的概念,并且不建议您阅读我的内容以保留重复的数据以适应您的查询。

采用以下示例(以mongoengine建模,但没关系):

    #User
    class User(Document):
        email = EmailFieldprimary_key=True)
        pswd_hash = StringField()
        #This also makes it easier to find the Projects the user has a Role
        roles = ListField(ReferenceField('Role')

    #Project
    class Project(Document):
        name = StringField()
        #This is probably unnecessary as the Role id is already the project id
        roles = ListField(ReferenceField('Role'))

    #Roles in project
    class Role(Document):
        project = ReferenceField('Project', primary_key=True)
        #List of permissions
        permissions = ListField(StringField())
        users = ListField(ReferenceField('User')

项目用户

每个项目中可以包含许多角色

每个用户在一个项目中可以有一个角色


因此,用户项目

之间有很多

用户角色

之间的多选

角色项目

之间的多选

问题是,当我尝试使模式适应访问时,因为在应用程序的每个页面刷新时,我都需要:

  1. 项目(ID在URL中)
  2. 用户(电子邮件正在会话中)
  3. 该项目中的用户权限(服务器端安全检查)

因此,考虑到这是最常见的查询,我应该如何为我的模式建模以适应它?

或者我现在的方式还可以吗?

3 个答案:

答案 0 :(得分:1)

对此建模有不同的方法,对于这种特殊的用例,我建议将角色/权限嵌套在项目文档中。

事实上,据我了解,您的角色并未在项目之间共享,因此有机会嵌入其中,以及项目角色与用户之间的映射。这是我的建议(使用简化的类):

class User(Document):
    name = StringField()

class RoleDefinition(EmbeddedDocument):
    users = ListField(ReferenceField(User))
    permissions = ListField(StringField())

class Project(Document):
    role_definitions = EmbeddedDocumentListField(RoleDefinition)

    def has_user_permission(self, user_id, permission):
        for role_def in self.role_definitions:
            if permission in role_def.permissions:
                return user_id in [us.id for us in role_def._data['users']]    # optimization to avoid to dereference all the users
        return False

# save a sample
bob = User(name='Bob').save()
hulk = User(name='hulk').save()
project = Project(
    role_definitions=[
        RoleDefinition(permissions=['read_file', 'delete_file'], users=[bob]),
        RoleDefinition(permissions=['upload_file'], users=[hulk])
    ]
).save()

# Check if a user has a certain permission in a project
assert project.has_user_permission(bob.id, 'read_file') is True

将使用以下结构保存文档:

{  
   '_id':ObjectId('5d2cd78cd97f1cc85d0b7b28'),
   'role_definitions':[  
      {  
         'permissions':['read_file', 'delete_file'],
         'users':[ObjectId('5d2cd5d6d97f1cc85d0b7b26')]
      },
      {  
         'permissions':['upload_file'],
         'users':[ObjectId('5d2cd5d9d97f1cc85d0b7b27')]
      }
   ]
}

然后您可以通过以下查询来验证具有特定ID的用户在项目中是否具有特定权限:

def user_has_permission_in_project(project_id, user_id, permission):
    qry = Project.objects(id=project_id,
                          role_definitions__elemMatch={'users': user_id, 'permissions': permission})
    return qry.count() > 0

assert user_has_permission_in_project(project.id, bob.id, 'read_file') is True

假设它适合您的约束,那么您应该能够根据需要进行调整

答案 1 :(得分:1)

有多种方法可以以当前形式对需求进行建模。

如果没有太多重复,并且在请求文档时始终需要嵌入的数据,则可以使用嵌入的文档。

在您的情况下,我将使用参考。您的结构总体上对我来说看起来不错。

我将尝试向您展示一种这样的方式,并将$lookupreferences一起使用。您应该尝试使用三个单独的集合,分别用于每个项目,角色和用户,如下所示。

另一个选择是使用$DBRef,当您获取项目集合时,它将急切加载项目中的所有角色。此选项将取决于mongoengine驱动程序,我确定驱动程序会支持。

项目文档(已从项目中删除角色)

{ "_id": ObjectId("5857e7d5aceaaa5d2254aea2"),
  "name": "newProject"
}

角色文档

{ "_id" : "role1",
  "project": ObjectId("5857e7d5aceaaa5d2254aea2"); 
  "users": ["email1", "email2"],
  "permissions": ["delete","update"]
}
{ "_id" : "role2",
  "project": ObjectId("5857e7d5aceaaa5d2254aea2"); 
  "users": ["email1"],
  "permissions": ["add"]
}

用户文档

{ "email" : "email1",
  "roles": ["role1", "role2"]
}
{ "email" : "email2",
  "roles": ["role1"]
}

显示所有项目

db.project.find({})

获取项目中的所有角色

db.role.aggregate([
 {$match: {project:ObjectId("5857e7d5aceaaa5d2254aea2")} },
])

回复

{
    "_id": ObjectId("5857e7d5aceaaa5d2254aea2"),
    "name": "newProject",
    "roles": [
       { "_id" : "role1",
         "users": ["email1", "email2"]
       },
       { "_id" : "role2",
         "users": ["email1"]
       }
    ]
}

获取用户的所有角色

db.user.aggregate([ 
  {$match: {email:"email1"}},
  {$lookup: {
     from: "role",
     localField: "roles",
     foreignField: "_id",
     as: "roles"
   }}
])

回复

{
    "email": "email1",
    "roles": [
       { "_id" : "role1",
         "users": ["email1", "email2"]
       },
       { "_id" : "role2",
         "users": ["email1"]
       }
    ]
}

获取项目ID和电子邮件ID的用户权限(具有当前结构)

db.role.aggregate([
  {$match: {_id:ObjectId("5857e7d5aceaaa5d2254aea2")}},
  {$match: {"$expr": {"$in": ["email1", "$users"]}}},
  {$project:{"permissions":1}}
 ])

回复

[
  {
      "permissions": ["delete","add"]
  },
  {
      "permissions": ["update"]
  }
]

随着用户的增加,您可以从角色集合中删除用户,并且可以使用$ lookup将用户加入角色集合以标识项目。

角色文档(已从角色中删除用户)

{ "_id" : "role1",
  "project": ObjectId("5857e7d5aceaaa5d2254aea2"); 
  "permissions": ["delete","update"]
}
{ "_id" : "role2",
  "project": ObjectId("5857e7d5aceaaa5d2254aea2"); 
  "permissions": ["add"]
}

用户文档

{ "email" : "email1",
  "roles": ["role1", "role2"]
}
{ "email" : "email2",
  "roles": ["role1"]
}

获取项目ID和电子邮件ID的用户权限(具有更新的结构)(首选)

db.user.aggregate([
  {$match: {email:"email1"}},
  {$lookup: {
     from: "role",
     localField: "roles",
     foreignField: "_id",
     as: "roles"
   }},
   {$unwind: "$roles"},
   {$match: {"roles.project": ObjectId("5857e7d5aceaaa5d2254aea2")}},
   {$project:{"permissions":"$roles.permissions"}}
 ])

回复

[
  {
      "permissions": ["delete","update"]
  },
  {
      "permissions": ["add"]
  }
]

答案 2 :(得分:1)

通常,您可以通过两种方式对权限进行建模。要么有静态角色,这些角色都具有执行某些操作的隐式权限。或者,有些角色仅仅是获得明确权限的容器。

隐式权限

文档的大小限制为16MB,因此除非您有 lot 个用户和一个 lot 个角色,否则无需进行标准化。

{
 "_id": new ObjectID(),
 "name": "My Project",
 "roles": [
   {
     "role": "admin",
     "members": ["foo","bar"]
   },
   {
     "role": "user",
     "members": ["baz","foo"]
   }
 ]
}

这里具有简单数据模型的另一种方法是每个关系只有一个文档:

{"project":someObjectId,"role":"admin","user":"foo"}
{"project":someObjectId,"role":"admin","user":"bar"}
{"project":someObjectId,"role":"user","user":"baz"}

现在,您大概已经了解了您的项目,因此您可以轻松查询特定用户的角色:

db.roles.find({"project":currentProjectId,"user":currentUser})

如果用户可以具有多个角色,则可以进行汇总,例如:

// Add to above data
// db.roles.insert({"project":ObjectId("5d2f6f0fd2c6b42117ecbbe5"),role:"user",user:"foo"})
db.roles.aggregate([{
  $match:{
    user:"foo",
    project:ObjectId("5d2f6f0fd2c6b42117ecbbe5")
  }},{
  $group:{
    "_id":"$user",
    roles:{$addToSet:"$role"}
  }}
])

// Result
{ "_id" : "foo", "roles" : [ "user", "admin" ] }

使用userproject上的复合索引(顺序很重要!),此聚合查询应该足够了。

显式权限

首先,我们必须定义如何设置显式权限。一种可靠的方法是使用

domain:action[,action...]:instance

(公然摘自Apache Shiro's permission model)。在不完全知道要使用应用程序实现什么的情况下很难建模,但是为了举例说明,让我们假设有一个更改任何项目标题的权限。因此,抽象描述为:

project:editTitle:*

如果您不需要实例级权限,它将变得更加容易:

project:editTitle

这很容易解析,可以将角色定义为

{
  "_id":"editor",
  "permissions":[
    "project:editTitle",
    "project:addUser",
    "project:stop",
    "project:andSoOnAndSoForth",
    "comment:dlete"
  ]
}

嘿,等等,有错字!让我们更正它:

db.permissions.update(
  {permissions:"comment:dlete"},
  {$set:{"permissions.$":"comment:delete"}}
)

(如果您也想重新设置权限,这很方便-只是不要忘记添加{multi:true}作为第三个参数)。

现在赋予类似角色

{ "project" : ObjectId("5d2f6f0fd2c6b42117ecbbe5"), "role" : "admin", "user" : "foo" }
{ "project" : ObjectId("5d2f6f0fd2c6b42117ecbbe5"), "role" : "admin", "user" : "bar" }
{ "project" : ObjectId("5d2f6f0fd2c6b42117ecbbe5"), "role" : "user", "user" : "baz" }
{ "project" : ObjectId("5d2f6f0fd2c6b42117ecbbe5"), "role" : "user", "user" : "foo" }
{ "project" : ObjectId("5d2f6f0fd2c6b42117ecbbe5"), "role" : "editor", "user" : "baz" }

和类似权限

{ "_id" : "editor", "permissions" : [ "project:editTitle", "project:addUser", "project:stop", "project:andSoOnAndSoForth", "comment:delete" ] }
{ "_id" : "user", "permissions" : [ "*:read" ] }
{ "_id" : "admin", "permissions" : [ "*:*" ] }

您可以通过

获得用户对项目的明确许可
db.roles.aggregate([
    // we only want to get the roles of the current user for a certain project
    { $match: { user: "baz", project: ObjectId("5d2f6f0fd2c6b42117ecbbe5") } },
    // We get the permissions associated with the role
    { $lookup: { from: "permissions", localField: "role", foreignField: "_id", as: "permissionDocs" } },
    // We pull the permissions into the root document...
    { $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ["$permissionDocs", 0] }, "$$ROOT"] } } },
    // ... and get rid of all the stuff we do not need
    { $project: { permissionDocs: 0, role: 0, project: 0 } },
    // We flatten the various permission arrays of the result documents...
    { $unwind: "$permissions" },
    // ... and finally construct our set of permissions
    { $group: { "_id": "$user", permissions: { $addToSet: "$permissions" } } }
])

// Result:
{ "_id" : "baz", "permissions" : [ "comment:delete", "project:andSoOnAndSoForth", "*:read", "project:editTitle", "project:addUser", "project:stop" ] }

使用该结果,您可以简单地遍历一组权限并允许删除注释,例如,如果权限*:*comment:*comment:delete中的任何一个存在。

请注意,我没有规范角色的权限。这为我们省去了一个常见用例的额外查找,但代价是一个罕见的用例(更改权限域或操作)比较慢。

编辑:

您可以将其包装为以下功能:

function hasPermission(user, project, permission) {
    var has = db.roles.aggregate([{
        $match: {
            user: user,
            project: project
        }}, {
        $lookup: {
            from: "permissions",
            localField: "role",
            foreignField: "_id",
            as: "permissionDocs"
        }}, {
        $replaceRoot: {
            newRoot: {
                $mergeObjects: [{
                    $arrayElemAt: ["$permissionDocs", 0]
                }, "$$ROOT"]
            }
        }}, {
        $project: {
            permissionDocs: 0,
            role: 0,
            project: 0
        }}, {
        $unwind: "$permissions"
        }, {
        $group: {
            "_id": "$user",
            permissions: {
                $addToSet: "$permissions"
            }
        }
    }, {
        $match: {
            "permissions": permission
        }
    }]);
    return has.toArray().length > 0
}

这样的东西:

> if ( hasPermission("baz",ObjectId("5d2f6f0fd2c6b42117ecbbe5"),"comment:delete") ) {
    print("Jay")
  } else {
    print("Nay")
  }

产生Yay。 (请注意,您需要扩展该函数以匹配通配符权限comment:**:*。)