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

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

Спасибо.

Ответ 1

Первая часть/ответ (реализация 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();

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

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

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

Вторая часть/ответ (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 Model является слоем, который содержит много классов. Большая часть классов может быть разделена на две группы на основе ответственности:

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

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

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

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

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

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

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

- Услуги

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

Подведем итог к этому вопросу:

  • Command
  • ACL
  • Пользователь

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

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

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

Итак, теперь понятен контекст того, как эти три части (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'ing здесь: если пользователь запрашивает команду, которая не существует, вы не позволяете этой команде выполнять. Итак, где-нибудь, что случается в вашем приложении, может быть хорошим местом для добавления "реальных" проверок 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 в вашем случае управляет доступом к команде, а не самой командой.

Ответ 3

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

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

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

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