MongoDB: как разработать схему на основе шаблонов доступа к приложениям?

Как человек, пришедший из 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. Проект (идентификатор находится в URL-адресе)
  2. Пользователь (электронная почта находится в сеансе)
  3. Разрешения пользователя в этом проекте (проверки безопасности на стороне сервера)

Итак, учитывая, что это самый распространенный запрос, как мне смоделировать мою схему, чтобы вместить его?

Или то, как я сейчас поступаю, уже нормально?

# Это, вероятно, не нужно, поскольку идентификатор роли уже является идентификатором проекта.. В проекте несколько ролей. Таким образом, ListField необходим в проекте для извлечения ролей. Вы должны создать новый идентификатор роли (первичный ключ) и не может совпадать с идентификатором проекта (внешний ключ) и назначить ссылки ролей на идентификатор роли. У вас есть страница, где вы показываете все проекты? и детали проекта имеют роли? роли имеют пользователей? Просто пытаюсь понять ваш поток пользовательского интерфейса. Вам нужно получить все данные сразу? Вы ищете отображение всех проектов для пользователя, вошедшего в систему?
s7vr 15.07.2019 16:22

@ user2683814 Да, есть страница, на которой я показываю все проекты, все роли в ней и всех пользователей в каждой роли, но, вероятно, она будет разделена на разные этапы, не нужно получать все сразу.

Mojimi 15.07.2019 16:26

Вы можете удалить роли из коллекции проектов, а также удалить пользователей из коллекции ролей, поскольку роли и пользователи постоянно растут. Теперь вы можете получить роли для проекта, запросив коллекцию ролей в проекте и роли для пользователя, используя поиск. У вас есть только одна роль для каждого проекта? Почему проект является первичным ключом в роли? Я предполагаю, что это не так. Я также обновил ответ, чтобы включить эти изменения. Взгляните и дайте мне знать, что вы думаете.

s7vr 16.07.2019 16:18

Ваша авторизация основана на роли или разрешении? Другими словами: достаточно ли иметь роль администратора или роль администратора является просто контейнером для разрешений?

Markus W Mahlberg 17.07.2019 21:30

@MarkusWMahlberg роль администратора - это просто контейнер для разрешений.

Mojimi 17.07.2019 21:33
Использование JavaScript и MongoDB
Использование JavaScript и MongoDB
Сегодня я собираюсь вкратце рассказать о прототипах в JavaScript, а также представить и объяснить вам работу с базой данных MongoDB.
0
5
4 888
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Существуют разные способы моделирования этого, для этого конкретного случая использования я бы предложил вложить роли/разрешения в документы проекта.

На самом деле, насколько я понимаю, ваши роли не распределяются между проектами, поэтому есть возможность встроить это, а также сопоставление между ролями проекта и пользователями. Вот мое предложение (с использованием упрощенных классов):

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')]
      }
   ]
}

Затем вы можете проверить, имеет ли пользователь с определенным идентификатором определенное разрешение в проекте, с помощью следующего запроса:

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

Предполагая, что это соответствует вашим ограничениям, вы сможете адаптировать это к своим потребностям.

Я категорически не согласен с тем, что вложение структур является лучшей практикой. На самом деле это общая проблема с моделями данных MongoDB, которая называется чрезмерным внедрением. Это совершенно смертельно для некоторых случаев использования (а именно, когда чрезмерно встроенный документ достигает предела размера документа в 16 МБ).

Markus W Mahlberg 17.07.2019 23:04

Удачи вам в заполнении лимита в 16 Мб только с разрешениями. Если вы не разрабатываете новый Twitter, все должно быть в порядке. Но вы правы, есть предел.

bagerard 17.07.2019 23:33

«Идеальный способ организации вещей в mongodb — это вложенная структура, когда это возможно». Это довольно общее. А пока наверняка есть допустимые варианты использования для встраивания, правильнее сказать, что его следует использовать только для 1:очень немногих отношений, чем использовать их как наилучшую практику. Вот это я и хотел отметить.

Markus W Mahlberg 18.07.2019 00:00

Существуют различные способы моделирования вашего требования в текущей форме.

Вы можете использовать встроенные документы, если у вас не так много дублирования, и вам всегда нужны встроенные данные, когда вы запрашиваете документ.

В вашем случае я бы использовал ссылки. Ваша структура в целом выглядит хорошо для меня.

Я постараюсь показать вам один из таких способов и использовать $lookup с references. Вы должны попробовать с тремя отдельными коллекциями, по одной для каждого проекта, роли и пользователя, как показано ниже.

Еще один вариант — использовать $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"]
       }
    ]
}

Получить разрешения пользователя для идентификатора проекта и идентификатора электронной почты (с текущей структурой)

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

Получить разрешения пользователя для идентификатора проекта и идентификатора электронной почты (с обновленной структурой) (предпочтительно)

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

Обратите внимание, что не все драйверы поддерживают прозрачную загрузку dbref: docs.mongodb.com/manual/reference/database-references/…

Markus W Mahlberg 17.07.2019 23:16
Ответ принят как подходящий

Как правило, вы можете моделировать разрешения двумя способами. Либо есть статические роли, которые имеют неявные разрешения на выполнение определенных действий. Или есть роли, которые являются простыми контейнерами для явных разрешений.

Неявные разрешения

Размер документов ограничен 16 МБ, поэтому, если у вас нет много пользователей И много ролей, нормализация не требуется.

{
 "_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" ] }

С составным индексом на user и project (порядок имеет значение!), этого запроса агрегации должно быть достаточно.

Явные разрешения

Во-первых, мы должны определить, как мы хотим настроить наши явные разрешения. Надежный способ - использовать

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

(нагло взято из Модель разрешений Apache Shiro). Довольно сложно смоделировать это, не зная точно, чего вы хотите достичь с помощью своего приложения, но в качестве примера давайте предположим, что у любого проекта есть разрешение на изменение названия. Таким образом, абстрактное описание будет таким:

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:* и *:*.)

Другие вопросы по теме