Сразу опишу тему топика. В топике поднимаются вопросы по уровням политик доступов в MODX. В MODX есть два метода проверки прав:
$modx->hasPermission() — самый часто используемый метод. Но этот метод по сути — сокращенная запись $modx->context->checkPolicy();
public function hasPermission($pm) {
$state = $this->context->checkPolicy($pm);
return $state;
}
Но если учесть, у нас обязательно есть текущий контекст ($modx->content) и в момент времени он всегда один, то метод $modx->hasPermission() условно можно расценивать как глобальную проверку прав. К примеру «а есть ли у пользователя право удалять документы» (политика delete_document). Но это касается всех документов в принципе. То есть если у пользователя нет этого права, то он не сможет удалить документы (вот это я заявляю условно, так как это только если через системные процессоры, в которых прописана проверка на это право. Если мы сделаем выборку каких-то документов через API и с каждым из них выполним ->remove(), эта настройка нам никак не мешает, так как она касается объекта modContext, а не modResource).
Но если нам надо проверить права на удаление конкретного документа? Вот как раз с конкретным документом и надо выполнять проверку $object->checkPolicy('remove'); И об этом и пойдет речь в топике.
Как уже говорилось ранее, все активные объекты в MODX и xPDO — это расширения класса xPDOObject, который содержит все необходимые методы для работы с базой данных, установкой значений и доступа к ним и т.п. Класс xPDOSimpleObject, который расширяет класс xPDOObject, только добавляет информацию о колонке id и первичном ключе для нее. То есть если у вас есть класс, в таблице которого есть колонка id, и которая является первичным ключом, то можно просто расширить не xPDOObject, а сразу xPDOSimpleObject, и тогда не придется в мап-файле описывать колонку id, она уже унаследуется от xPDOSimpleObject. Эти классы содержатся в самом пакете xPDO.
Но в MODX есть и еще один интересный класс, расширяющий xPDOObject — modAccessibleObject. Этот класс содержит в себе базовые механизмы проверки доступов пользователя к объектам классов, наследующих этот класс. Немного забегая вперед, сразу отмечу, что класс modAccessibleSimpleObject, расширяющий класс modAccessibleObject — это тоже самое, что xPDOSimpleObject, расширяющий xPDOObject — то есть он не перегружает никаких методов и т.п., а просто сразу описывает колонку id.
Пример. Допустим у нас есть класс test, расширяющий modAccessibleSimpleObject.
class test extends modAccessibleSimpleObject{
}
Этот класс уже унаследует от modAccessibleSimpleObject следующие методы: _loadInstance, load, save, remove, checkPolicy, findPolicy и другие (перечислил самые важные для нашей темы).
Эти методы будут использоваться для различных уровней проверки прав в конечным объектам.
Сразу отмечу, что большинство используемых методов рассчитаны только на работу уже с конечными объектами (что может повлиять на разницу подсчета строк в базе данных, и конечным числом полученных объектов). То есть при выборке записей из базы данных xPDO не формирует автоматически запрос так, чтобы исключить записи для тех объектов, к которым у пользователя нет доступов. xPDO сделает выборку всех записей, и только потом для каждой записи он постарается получить инстанс объекта в методе xPDOObject::_loadInstance(). Но в классе modAccessibleObject метод loadInstance перегружен. Давайте посмотрим код.
public static function _loadInstance(& $xpdo, $className, $criteria, $row) {
/** @var modAccessibleObject $instance */
$instance = xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
// print_r($instance->toArray());
if ($instance instanceof modAccessibleObject && !$instance->checkPolicy('load')) {
if ($xpdo instanceof modX) {
$userid = $xpdo->getLoginUserID();
if (!$userid) $userid = '0';
$xpdo->log(xPDO::LOG_LEVEL_INFO, "Principal {$userid} does not have permission to load object of class {$instance->_class} with primary key: " . (is_object($instance) && method_exists($instance,'getPrimaryKey') ? print_r($instance->getPrimaryKey(), true) : ''));
}
$instance = null;
}
return $instance;
}
То есть xPDO получит конечный объект, и если этот объект является инстансом класса modAccessibleObject, он будет проверять его на право загрузки ('load'). И если нет права на загрузку этого объекта, то он обнулит этот объект и запишет ошибку доступа.
К слову, именно этот момент объясняет, почему если создать закрытую группу ресурсов с доступом для определенных групп пользователей, то неавторизованный пользователь просто так не получает сообщение «Доступ закрыт» при попытке зайти на эту страницу. Все потому, что у неавторизованного пользователя нет прав доступов 'load' к этой группе ресурсов, и MODX просто не может получить этот объект, чтобы проверить есть у пользователя право просматривать эту страницу или нет. Так что создавая группу ресурсов, обязательно давайте права 'load' для anonimus. Но не забывайте и про другие группы пользователей :-) Легко может получиться так, что анонимусы будут иметь больше прав. Анонимусам дали права 'load', тем группам пользователей, которые должны иметь права на просмотр, тоже права дали, а про другие группы пользователей забыли. И получается, что пользователь из другой группы еще будучи не авторизованным, заходя на страницу, видит, что доступ закрыт, а как только авторизовался, вообще стал видеть 404 (страница не найдена), и все потому что из группы anonimus он попал в авторизованную группу, которая вообще не имеет права на ресурс.
public function findPolicy($context = '') {
return array();
}
Как мы видим, метод modAccessibleObject::findPolicy() возвращает пустой массив, а значит изначально все все объекты доступны (не имеют никаких политик). И здесь начинается самое интересно.
Наборы уровней доступов прописываются уже на уровне конкретных классов, переопределяя метод modAccessibleObject::checkPolicy(). Давайте, к примеру, посмотрим метод modResource::checkPolicy(). Как видите, в этом методе уже прописан поиск записей настроек доступов к группам ресурсов.
Здесь сразу следует отметить еще одно правило: если не настроено ни одного правила доступа, то разрешено всем и все. Если есть хоть одно правило, то сразу действует правило «если явно не разрешено, то запрещено». Это значит, что если вы хоть для кого-то настроили какие-то права, то могут потребоваться и настройки для остальных групп пользователей (или объектов).
То есть, если вы пишите свой класс, и хотите в нем как-то воздействовать на доступы, то вам как минимум нужно перегрузить метод modAccessibleObject::findPolicy(), чтобы он возвращал массив доступов. Но можно конечно и метод modAccessibleObject::checkPolicy() перегружать, чтобы по какой-то своей логике возвращать true или false.
Это была основная часть вопроса, но еще не вся. Объем здесь действительно огромный, я итак сокращаю как могу. Просто слишком много всего взаимосвязано, но картину надо изложить полностью. Далее мы вкратце рассмотрим стандартный механизм управления доступами. Вот мы настроили свои доступы, прописали везде где что надо, и теперь хотим, чтобы стандартный метод modAccessibleObject::checkPolicy() отрабатывал проверку прав без каких-либо вмешательств в него. Здесь важно понять, что базовый механизм основывается на сессиях. То есть права пользователя не дергаются каждый раз из базы данных, а все содержится в массиве сессии. Вот кусочек кода из modAccessibleObject::checkPolicy()
$policy = $this->findPolicy();
if (!empty($policy)) {
$principal = $this->xpdo->user->getAttributes($targets);
if (!empty($principal)) {
foreach ($policy as $policyAccess => $access) {
foreach ($access as $targetId => $targetPolicy) {
foreach ($targetPolicy as $policyIndex => $applicablePolicy) {
То есть, если были получены настройки прав доступа к объекту, то xPDO пытается получить атрибуты объекта пользователя ($this->xpdo->user->getAttributes($targets)), и в них найти прописанные права. Метод getAttributes содержится в классе modPrincipal.
public function getAttributes($targets = array(), $context = '', $reload = false) {
$context = !empty($context) ? $context : $this->xpdo->context->get('key');
if (!is_array($targets) || empty($targets)) {
$targets = explode(',', $this->xpdo->getOption('principal_targets', null, 'modAccessContext,modAccessResourceGroup,modAccessCategory,sources.modAccessMediaSource'));
array_walk($targets, 'trim');
}
$this->loadAttributes($targets, $context, $reload);
return $this->_attributes[$context];
}
И вот здесь важный момент: все перечисленные классы доступов, которые MODX собирает в сессию пользователя, содержатся в системной настройке principal_targets. То есть, если вы хотите, чтобы еще и вашим какие-то записи собирались в сессию пользователя (в нашем случае какие-то из них еще и используются для проверки прав), то надо их дописать в системную настройку.
И напоследок: вариант с хранением в сессиях менее ресурсоемкий, так как не требует постоянных обращений в базу данных, а дергает информацию из сессии, но и менее гибкий, так как необходимо переписывать сессию, чтобы изменения в политиках пользователя вступили в силу. Потому если требуются более динамические методы проверки прав, то надо смотреть в сторону перегрузки метода modAccessibleObject::checkPolicy(), где вы на свое усмотрение можете сразу вернуть true или false;
У кого какие вопросы, обязательно спрашивайте, так как этот материал надо понимать досконально. Считайте его тестером ваших знаний. Если вам здесь что-то не понятно, значит еще обязательно надо заполнять пробелы в знаниях.