MongoDB:如何根据应用程序访问模式设计模式?

Moj*_*imi 0 database-design mongodb 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')
Run Code Online (Sandbox Code Playgroud)

项目用户

每个项目中可以有多个角色

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


所以,这是用户项目之间的多对多

用户角色之间的多一

角色项目之间的多一


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

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

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

或者我现在的方式已经可以了吗?

Mar*_*erg 9

通常,您可以通过两种方式对权限建模。或者,有静态角色,它们具有执行某些操作的隐式权限。或者有些角色仅仅是显式权限的容器。

隐式权限

文档有 16MB 的大小限制,因此除非您有很多用户和很多角色,否则没有必要进行规范化。

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

在此处使用简单数据模型的另一种方法是为每个关系创建一个文档:

{"project":someObjectId,"role":"admin","user":"foo"}
{"project":someObjectId,"role":"admin","user":"bar"}
{"project":someObjectId,"role":"user","user":"baz"}
Run Code Online (Sandbox Code Playgroud)

现在,您大概知道您的项目,因此您可以像这样简单地查询特定用户的角色:

db.roles.find({"project":currentProjectId,"user":currentUser})
Run Code Online (Sandbox Code Playgroud)

如果用户可以有多个角色,您可以进行聚合,例如:

// 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" ] }
Run Code Online (Sandbox Code Playgroud)

使用userproject(顺序很重要!)的复合索引,这个聚合查询应该是最充分的。

显式权限

首先,我们必须定义我们希望如何设置我们的显式权限。一个健壮的方法是使用

domain:action[,action...]:instance
Run Code Online (Sandbox Code Playgroud)

(公然取自Apache Shiro 的权限模型)。在不确切知道您希望通过应用程序实现什么目标的情况下很难进行建模,但为了举例,让我们假设允许更改任何项目的标题。所以抽象的描述是:

project:editTitle:*
Run Code Online (Sandbox Code Playgroud)

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

project:editTitle
Run Code Online (Sandbox Code Playgroud)

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

{
  "_id":"editor",
  "permissions":[
    "project:editTitle",
    "project:addUser",
    "project:stop",
    "project:andSoOnAndSoForth",
    "comment:dlete"
  ]
}
Run Code Online (Sandbox Code Playgroud)

嘿,等等,有一个错字!让我们更正一下:

db.permissions.update(
  {permissions:"comment:dlete"},
  {$set:{"permissions.$":"comment:delete"}}
)
Run Code Online (Sandbox Code Playgroud)

(如果您也想改写权限,这很方便——只是不要忘记添加{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" }
Run Code Online (Sandbox Code Playgroud)

和权限,如

{ "_id" : "editor", "permissions" : [ "project:editTitle", "project:addUser", "project:stop", "project:andSoOnAndSoForth", "comment:delete" ] }
{ "_id" : "user", "permissions" : [ "*:read" ] }
{ "_id" : "admin", "permissions" : [ "*:*" ] }
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式获得用户对项目的明确权限

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" ] }
Run Code Online (Sandbox Code Playgroud)

有了这个结果,您可以简单地迭代权限集并允许删除评论,例如,如果存在任何一个权限*:*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
}
Run Code Online (Sandbox Code Playgroud)

所以像:

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

结果在Yay. (请注意,您需要扩展函数以匹配通配符权限comment:**:*。)