Лучший способ отфильтровать доступ к действиям контроллера в соответствии с конкретным идентификатором клиента

Используя CakePHP 2.2, я создаю приложение, в котором каждый клиент имеет свою собственную «область» данных, и никакие другие данные им не видны. Например, у клиента есть свой набор пользователей, курсов, подрядчиков и вакансий. Группы являются общими для клиентов, но они не могут выполнять действия над группами. Все, что клиенты могут делать с группами, это назначать их пользователям. Таким образом, администратор (используя ACL) может управлять данными только с одного и того же идентификатора клиента.

Все мои объекты (кроме групп, конечно) имеют ключ client_id.

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

Вот как я это делаю сейчас:

1- Авторизуется пользователь. Его client_id записывается в сессию по данным из таблицы пользователя.

$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);

2- В AppController у меня есть защищенная функция, которая сравнивает этот идентификатор сеанса с заданным параметром.

protected function clientCheck($client_id) {
    if ($this->Session->read('User.client_id') == $client_id) {
        return true;
    } else {
        $this->Session->setFlash(__('Invalid object or view.'));
        $this->redirect(array('controller' => 'user', 'action' => 'home'));
    }
}

3- Я использую разные действия с индексами (каждый индекс, каждый соответствующий контроллер), я проверяю client_id, используя условие разбивки на страницы.

public function index() {
    $this->User->recursive = 0;
    $this->paginate = array(
         'conditions' => array('User.client_id' => $this->Session->read('User.client_id'))
    );
    $this->set('users', $this->paginate());
}

4- В других действиях я проверяю client_id перед проверкой типа HTTP-запроса таким образом.

$user = $this->User->read(null, $id);
$this->clientCheck($user['User']['client_id']);
$this->set('user', $user);

person IanBussieres    schedule 06.11.2012    source источник


Ответы (3)


Концепция хороша — она не «грязная», и она почти такая же, как я справлялся с подобными ситуациями.

Вы только что получили пару строк избыточного кода. Первый:

$this->Auth->user('id')

Этот метод может фактически получить любое поле для вошедшего в систему пользователя, поэтому вы можете сделать:

$this->Auth->user('client_id')

Итак, ваши две строки:

$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);

Не нужны. Вам не нужно перечитывать пользователя или записывать что-либо в сеанс — просто возьмите client_id непосредственно из Auth в любое время, когда вам это нужно.

На самом деле, если вы прочтете http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user там даже говорится, что вы можете получить его вне контекста контроллер, используя статический метод, например:

AuthComponent::user('client_id')

Хотя, похоже, вам это не понадобится.

person joshua.paling    schedule 06.11.2012
comment
Ничего себе, хотя вы в основном говорите, что я на правильном пути, ваш ответ на самом деле весьма полезен. Я обязательно уменьшу перезагрузку записи без своих контроллеров. Я подожду и посмотрю, какие ответы могут появиться, прежде чем поставить вам галочку. Спасибо - person IanBussieres; 07.11.2012

Вы также можете применить условие client_id ко всем находкам для Модели, поместив что-то в функцию beforeFind в Модели.

Например, в вашей модели User вы можете сделать что-то вроде этого:

function beforeFind( $queryData ) {

    // Automatically filter all finds by client_id of logged in user
    $queryData['conditions'][$this->alias . '.client_id'] = AuthComponent::user('client_id');

    return $queryData;
}

Не уверен, что AuthComponent::user('client_id') работает в модели, но идею вы поняли. Это автоматически применит это условие к каждой находке в модели.

Вы также можете использовать beforeSave в модели, чтобы автоматически устанавливать этот client_id для вас в новых записях.

person Bill Rollins    schedule 07.11.2012
comment
Как бы вы поступили с ошибками в этом случае? (например, в методе, который он использует, если client_id неверен, он устанавливает быстрое сообщение и перенаправляет на домашнюю страницу.) В этом случае, если ваша находка ничего не вернула, как бы вы узнали, что client_id был неправильным, или если это было что-то еще (например, запись не существовала, независимо от идентификатора клиента)? - person joshua.paling; 08.11.2012
comment
Вы будете обрабатывать записи, не найденные в контроллере, как обычно. Преимущество включения этого в вашу модель заключается в том, что вам не нужно размещать это в каждом отдельном FIND, который вы делаете. Вы также можете переопределить функцию EXISTS. - person Bill Rollins; 08.11.2012
comment
Принцип отличный, но он будет конфликтовать, когда я хочу найти информацию о пользователе перед сеансом (и, следовательно, идентификатор клиента), например, при входе в систему или проверке доступности имени пользователя. - person IanBussieres; 10.11.2012

Мой ответ может быть специфичным для движка базы данных, поскольку я использую PostgreSQL. В моем проекте я использовал разные схемы для каждого клиента в терминах mysql, которые были бы отдельной базой данных для каждого клиента.

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

В конкретных моделях компании я определяю

public $useDbConfig = 'company_data';

В методе Controller/AppController.php beforeFilter() у меня есть этот код для установки схемы в соответствии с вошедшим в систему пользователем.

if ($this->Session->check('User.Company.id')) {
    App::uses('ConnectionManager', 'Model'); 
    $dataSource = ConnectionManager::getDataSource('company_data');
    $dataSource->config['schema'] = 
        'company_'.$this->Session->read('User.Company.id');
}

Как видите, я обновляю dataSource на лету в соответствии с используемой компанией. Это исключает любое участие company_id в любом запросе, поскольку в этой схеме (базе данных) хранятся только данные, относящиеся к компании. Также это добавляет возможность масштабирования проекта.

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

person icebreaker    schedule 07.11.2012