Как я могу реализовать список управления доступом в моем приложении Web MVC?

96

Первый вопрос

Объясните, пожалуйста, как в MVC можно реализовать самый простой ACL.

Вот первый подход к использованию Acl в контроллере ...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

Это очень плохой подход, и его минус в том, что мы должны добавлять фрагмент кода Acl в каждый метод контроллера, но нам не нужны дополнительные зависимости!

Следующий подход - создать все методы контроллера privateи добавить код ACL в __callметод контроллера .

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

Он лучше предыдущего кода, но основные минусы ...

  • Все методы контроллера должны быть закрытыми
  • Мы должны добавить код ACL в метод __call каждого контроллера.

Следующий подход - поместить код Acl в родительский контроллер, но нам все равно нужно сохранить все методы дочернего контроллера закрытыми.

Каково решение? А что лучше? Где я должен вызывать функции Acl, чтобы разрешить или запретить выполнение метода.

Второй вопрос

Второй вопрос касается получения роли с помощью Acl. Представим, что у нас есть гости, пользователи и друзья пользователя. Пользователь ограничил доступ к просмотру своего профиля, его могут просматривать только друзья. Все гости не могут просматривать профиль этого пользователя. Итак, вот логика ..

  • мы должны убедиться, что вызываемый метод является профилем
  • мы должны определить владельца этого профиля
  • мы должны определить, является ли зритель владельцем этого профиля или нет
  • мы должны прочитать правила ограничений для этого профиля
  • мы должны решить выполнять или не выполнять метод профиля

Главный вопрос - определение владельца профиля. Мы можем определить, кто является владельцем профиля, только выполнив метод модели $ model-> getOwner (), но Acl не имеет доступа к модели. Как мы можем это реализовать?

Надеюсь, что мои мысли ясны. Извините за мой английский.

Спасибо.

Кирзилла
источник
1
Я даже не понимаю, зачем вам «Списки контроля доступа» для взаимодействия с пользователем. Разве вы не могли бы просто сказать что-нибудь вроде if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(иначе, отобразите «У вас нет доступа к профилю этого пользователя» или что-то в этом роде? Я не понимаю.
Баттл Буткус
2
Наверное, потому что Кирзилла хочет управлять всеми условиями доступа в одном месте - в основном в конфигурации. Таким образом, любое изменение разрешений можно сделать в Admin, а не изменять код.
Mariyo

Ответы:

185

Первая часть / ответ (реализация ACL)

По моему скромному мнению, лучший способ приблизиться к этому - использовать шаблон декоратора. По сути, это означает, что вы берете свой объект и помещаете его внутри другого объекта, который будет действовать как защитная оболочка. Это НЕ потребует от вас расширения исходного класса. Вот пример:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

И вот как вы используете такую ​​структуру:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

Как вы могли заметить, это решение имеет несколько преимуществ:

  1. сдерживание может использоваться на любом объекте, а не только на экземплярах Controller
  2. проверка авторизации происходит вне целевого объекта, а это значит, что:
    • оригинальный объект не отвечает за контроль доступа, придерживается SRP
    • когда вы получаете "разрешение отклонено", вы не заблокированы внутри контроллера, больше возможностей
  3. вы можете внедрить этот защищенный экземпляр в любой другой объект, он сохранит защиту
  4. обернуть его и забыть его .. вы можете делать вид , что исходный объект, он будет реагировать тот же

Но с этим методом также есть одна серьезная проблема - вы не можете изначально проверить, реализует ли защищенный объект и интерфейс (что также применимо для поиска существующих методов) или является частью некоторой цепочки наследования.

Вторая часть / ответ (RBAC для объектов)

В этом случае главное отличие, которое вы должны осознавать, заключается в том, что ваши объекты домена (в примере:) Profileсами содержат информацию о владельце. Это означает, что для проверки того, имеет ли (и на каком уровне) пользователь к нему доступ, вам потребуется изменить эту строку:

$this->acl->isAllowed( get_class($this->target), $method )

По сути, у вас есть два варианта:

  • Предоставьте ACL для рассматриваемого объекта. Но вы должны быть осторожны, чтобы не нарушить Закон Деметры :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Запросите все необходимые сведения и предоставьте ACL только с тем, что ему нужно, что также сделает его немного более удобным для модульного тестирования:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )

Пара видеороликов, которые могут помочь вам придумать собственную реализацию:

Боковые примечания

Кажется, у вас довольно распространенное (и совершенно неправильное) понимание того, что такое модель в MVC. Модель - это не класс . Если у вас есть класс с именем FooBarModelили что-то, что наследует, AbstractModelзначит, вы делаете это неправильно.

В собственном MVC Модель - это слой, содержащий множество классов. Большую часть занятий можно разделить на две группы, исходя из ответственности:

- Бизнес-логика домена

( подробнее : здесь и здесь ):

Экземпляры из этой группы классов занимаются вычислением значений, проверкой различных условий, реализацией правил продаж и всем остальным, что вы бы назвали «бизнес-логикой». Они понятия не имеют, как хранятся данные, где они хранятся и даже существует ли хранилище.

Домен Бизнес-объект не зависит от базы данных. Когда вы создаете счет, не имеет значения, откуда поступают данные. Это может быть как из SQL, так и из удаленного REST API, или даже скриншот документа MSWord. Бизнес-логика не меняется.

- Доступ к данным и хранение

Экземпляры, созданные из этой группы классов, иногда называют объектами доступа к данным. Обычно структуры, реализующие шаблон Data Mapper (не путайте с ORM с таким же именем .. никакого отношения). Здесь будут находиться ваши операторы SQL (или, может быть, ваш DomDocument, потому что вы храните его в XML).

Помимо двух основных частей, следует упомянуть еще одну группу экземпляров / классов:

- Услуги

Здесь в игру вступают ваши и сторонние компоненты. Например, вы можете думать об «аутентификации» как об услуге, которая может быть предоставлена ​​вашим собственным или каким-то внешним кодом. Также "отправитель почты" может быть службой, которая может связывать воедино некоторый объект домена с PHPMailer или SwiftMailer или вашим собственным компонентом отправителя почты.

Другой источник услуг - это абстракция на уровне домена и доступа к данным. Они созданы для упрощения кода, используемого контроллерами. Например: создание новой учетной записи пользователя может потребовать работы с несколькими объектами домена и мапперами . Но при использовании службы ему потребуется всего одна или две строки в контроллере.

При создании сервисов вы должны помнить, что весь слой должен быть тонким . В сервисах нет бизнес-логики. Они существуют только для того, чтобы манипулировать объектом домена, компонентами и мапперами.

Одна из их общих черт заключается в том, что службы не влияют напрямую на уровень представления и являются настолько автономными, что могут использоваться (и часто прекращают работу) вне самой структуры MVC. Кроме того, такие самодостаточные структуры значительно упрощают переход на другую структуру / архитектуру из-за чрезвычайно низкой связи между сервисом и остальным приложением.

терешко
источник
34
Просто за 5 минут перечитав это, я узнал больше, чем за месяцы. Согласны ли вы с тем, что тонкие контроллеры отправляются в службы, которые собирают данные о просмотрах? Кроме того, если вы когда-нибудь будете отвечать на вопросы напрямую, отправьте мне сообщение.
Стефан
2
Частично согласен. Сбор данных из представления происходит вне триады MVC, когда вы инициализируете Requestэкземпляр (или какой-то его аналог). Контроллер только извлекает данные из Requestэкземпляра и передает большую их часть соответствующим службам (некоторые из них тоже отправляются на просмотр). Службы выполняют операции, которые вы им приказывали. Затем, когда представление формирует ответ, оно запрашивает данные у служб и на основе этой информации генерирует ответ. Указанный ответ может быть либо HTML, созданным из нескольких шаблонов, либо просто заголовком местоположения HTTP. Зависит от состояния, установленного контроллером.
tereško
4
Чтобы использовать упрощенное объяснение: контроллер «пишет» в модель и представление, представление «читает» из модели. Слой модели - это пассивная структура во всех веб-шаблонах, вдохновленных MVC.
tereško
@Stephane, что касается вопросов напрямую, вы всегда можете написать мне в твиттере. Или вы задали вопрос в виде "длинного текста", который нельзя втиснуть в 140 символов?
tereško
Читает из модели: означает ли это активную роль модели? Я никогда такого раньше не слышал. Я всегда могу отправить вам ссылку через твиттер, если вы так предпочитаете. Как видите, эти ответы быстро превращаются в разговоры, и я пытался с уважением относиться к этому сайту и вашим подписчикам в Twitter.
Стефан
16

ACL и контроллеры

Прежде всего: это чаще всего разные вещи / слои. Когда вы критикуете примерный код контроллера, он объединяет и то, и другое - очевидно, слишком тесно.

Терешко уже наметил способ, как вы можете отделить это больше с помощью шаблона декоратора.

Я бы сначала отступил на шаг назад, чтобы найти исходную проблему, с которой вы столкнулись, и затем немного ее обсудить.

С одной стороны, вы хотите иметь контроллеры, которые просто выполняют ту работу, которую им приказывают (команда или действие, назовем это командой).

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

Таким образом, для такого контроля доступа необходимо что-то еще, что объединяет эти два аспекта. В зависимости от контекста, в котором выполняется команда, срабатывает ACL, и необходимо принимать решения, может ли конкретная команда быть выполнена конкретным субъектом (например, пользователем).

Подведем итог тому, что у нас есть:

  • Команда
  • ACL
  • Пользователь

Компонент ACL здесь является центральным: он должен знать хотя бы что-то о команде (чтобы идентифицировать команду, чтобы быть точным), и он должен иметь возможность идентифицировать пользователя. Обычно пользователей легко идентифицировать по уникальному идентификатору. Но часто в веб-приложениях есть пользователи, которые вообще не идентифицируются, их часто называют гостями, анонимами, всеми и т. Д. В этом примере мы предполагаем, что ACL может использовать объект пользователя и инкапсулировать эти детали. Пользовательский объект привязан к объекту запроса приложения, и ACL может использовать его.

А как насчет определения команды? Ваша интерпретация шаблона MVC предполагает, что команда состоит из имени класса и имени метода. Если присмотреться, для команды есть даже аргументы (параметры). Итак, уместно спросить, что именно определяет команду? Имя класса, имя метода, количество или имена аргументов, даже данные внутри любого из аргументов или смесь всего этого?

В зависимости от того, какой уровень детализации вам нужен для идентификации команды в вашем ACL, это может сильно различаться. Для примера давайте оставим его просто и укажем, что команда идентифицируется по имени класса и имени метода.

Таким образом, контекст того, как эти три части (ACL, команда и пользователь) принадлежат друг другу, теперь более ясен.

Можно сказать, что с воображаемым компонентом ACL мы уже можем делать следующее:

$acl->commandAllowedForUser($command, $user);

Просто посмотрите, что здесь происходит: сделав и команду, и пользователя идентифицируемыми, ACL может выполнять свою работу. Работа ACL не связана с работой как пользовательского объекта, так и конкретной команды.

Не хватает только одной части, она не может жить в воздухе. И это не так. Итак, вам нужно найти место, где должен сработать контроль доступа. Давайте посмотрим, что происходит в стандартном веб-приложении:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

Чтобы найти это место, мы знаем, что оно должно быть до того, как будет выполнена конкретная команда, поэтому мы можем сократить этот список, и нам нужно только изучить следующие (потенциальные) места:

User -> Browser -> Request (HTTP)
   -> Request (Command)

В какой-то момент в вашем приложении вы знаете, что конкретный пользователь запросил выполнение конкретной команды. Здесь вы уже выполняете своего рода ACL: если пользователь запрашивает команду, которой не существует, вы не разрешаете выполнение этой команды. Поэтому, где бы это ни происходило в вашем приложении, может быть хорошим местом для добавления «настоящих» проверок ACL:

Команда найдена, и мы можем создать ее идентификацию, чтобы ACL мог с ней справиться. В случае, если команда не разрешена для пользователя, команда не будет выполнена (действие). Может быть, CommandNotAllowedResponseвместо этого CommandNotFoundResponseзапрос не может быть преобразован в конкретную команду.

Место, где отображение конкретного HTTPRequest отображается на команду, часто называется маршрутизацией . Поскольку у Routing уже есть задание по поиску команды, почему бы не расширить его, чтобы проверить, действительно ли команда разрешена для ACL? Например , посредством расширения Router к маршрутизатору осведомлены ACL: RouterACL. Если ваш маршрутизатор еще не знает User, то Routerэто не подходящее место, потому что для работы ACL необходимо идентифицировать не только команду, но и пользователя. Таким образом, это место может быть разным, но я уверен, что вы можете легко найти место, которое нужно расширить, потому что это место, которое выполняет требования пользователя и команды:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Пользователь доступен с самого начала, сначала команда с Request(Command).

Поэтому вместо того, чтобы помещать проверки ACL в конкретную реализацию каждой команды, вы помещаете ее перед ней. Вам не нужны какие-либо сложные шаблоны, магия или что-то еще, ACL выполняет свою работу, пользователь выполняет свою работу, и особенно команда выполняет свою работу: просто команда, ничего больше. Команде неинтересно знать, применимы ли к нему роли, охраняется он где-то или нет.

Так что просто держите отдельно вещи, которые не принадлежат друг другу. Используйте небольшую переформулировку принципа единой ответственности (SRP) : должна быть только одна причина для изменения команды - потому что команда изменилась. Не потому, что вы сейчас вводите ACL в свое приложение. Не потому, что вы переключаете объект User. Не потому, что вы переходите с интерфейса HTTP / HTML на интерфейс SOAP или командной строки.

ACL в вашем случае управляет доступом к команде, а не самой командой.

hakre
источник
Два вопроса: CommandNotFoundResponse и CommandNotAllowedResponse: могли бы вы передать их из класса ACL в маршрутизатор или контроллер и ожидать универсального ответа? 2: Если бы вы хотели включить метод + атрибуты, как бы вы с этим справились?
Стефан
1: Ответ - это ответ, здесь он не от ACL, а от маршрутизатора, ACL помогает маршрутизатору узнать тип ответа (не найден, особенно: запрещен). 2: Зависит. Если вы имеете в виду атрибуты как параметры из действий, и вам нужен ACL с параметрами, поместите их в ACL.
hakre
13

Одна из возможностей - обернуть все ваши контроллеры в другой класс, который расширяет Controller, и передать ему все вызовы функций обернутому экземпляру после проверки авторизации.

Вы также можете сделать это в диспетчере (если в вашем приложении он действительно есть) и искать разрешения на основе URL-адресов, а не методов управления.

изменить : нужен ли вам доступ к базе данных, серверу LDAP и т. д., это ортогонально вопросу. Я хотел сказать, что вы можете реализовать авторизацию на основе URL-адресов, а не методов контроллера. Это более надежно, потому что вы обычно не будете изменять свои URL-адреса (вид общедоступного интерфейса в области URL-адресов), но вы также можете изменить реализации своих контроллеров.

Как правило, у вас есть один или несколько файлов конфигурации, в которых вы сопоставляете определенные шаблоны URL-адресов с конкретными методами аутентификации и директивами авторизации. Диспетчер перед отправкой запроса на контроллеры определяет, авторизован ли пользователь, и прерывает отправку, если это не так.

Артефакто
источник
Не могли бы вы обновить свой ответ и добавить более подробную информацию о Диспетчере. У меня есть диспетчер - он определяет, какой метод контроллера мне следует вызвать по URL. Но я не могу понять, как мне получить роль (для этого мне нужен доступ к БД) в Диспетчере. Надеюсь скоро тебя услышать.
Кирзилла
Ага, понял твою идею. Я должен решить, разрешить выполнение или нет без доступа к методу! Недурно! Последний нерешенный вопрос - как получить доступ к модели из Acl. Любые идеи?
Кирзилла 07
@Kirzilla У меня такие же проблемы с контроллерами. Похоже, что где-то должны быть зависимости. Даже если ACL нет, как насчет уровня модели? Как сделать так, чтобы это не было зависимостью?
Стефан