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

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

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

Вот первый подход к использованию 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 нет доступа к модели. Как мы можем реализовать это?

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

Спасибо.

3 ответа

Решение

Первая часть / ответ (реализация 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 или вашим собственным компонентом отправителя почты.

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

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

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

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

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

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

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

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

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

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

Давайте подведем итог к этому моменту, что мы имеем:

  • команда
  • ACL
  • пользователь

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

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

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

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

Мы могли бы сказать, что с мнимым компонентом 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'ing в свое приложение. Не потому, что вы переключаете объект User. Не потому, что вы переходите с интерфейса HTTP/HTML на интерфейс SOAP или интерфейс командной строки.

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

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

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

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

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

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