Николай Ланец
30 мар. 2013 г., 1:36

modAccessibleObject в MODX. Проверка доступов к объектам.

Сразу опишу тему топика. В топике поднимаются вопросы по уровням политик доступов в MODX. В MODX есть два метода проверки прав:
  • MODx::hasPermission();
  • modAccessibleObject::checkPolicy();
$modx->hasPermission() — самый часто используемый метод. Но этот метод по сути — сокращенная запись $modx->context->checkPolicy();
public function hasPermission($pm) { $state = $this->context->checkPolicy($pm); return $state; }
Так что де-факто в MODX имеется только один механизм проверки прав — modAccessibleObject::checkPolicy(); Довольно подробно об этом я писал здесь: community.modx-cms.ru/blog/documentation/9209.html
Но если учесть, у нас обязательно есть текущий контекст ($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 он попал в авторизованную группу, которая вообще не имеет права на ресурс.
Теперь давайте рассмотрим метод modAccessibleObject::checkPolicy(); Для нас в нем пока самое главное — $this->findPolicy();
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;
У кого какие вопросы, обязательно спрашивайте, так как этот материал надо понимать досконально. Считайте его тестером ваших знаний. Если вам здесь что-то не понятно, значит еще обязательно надо заполнять пробелы в знаниях.
А чтобы свой объект сделать объектом modAccessibleSimpleObject (сейчас он xPDOSimpleObject), достаточно переписать тип объекта в мап-файле и в самом классе объекта?
Просто сейчас для проверки доступа сам прописал метод checkAccess (чтобы не путаться с checkPolicy — так как объект другой) и в процессоре приходится в метод beforeSet добавлять $this->object->checkAccess().
Было бы хорошо просто в объекте расширить метод checkPolicy и процессор оставить обычный, просто расширяющий modObjectUpdateProcessor.
А чтобы свой объект сделать объектом modAccessibleSimpleObject (сейчас он xPDOSimpleObject), достаточно переписать тип объекта в мап-файле и в самом классе объекта?
Достаточно просто класс расширить modAccessibleSimpleObject, так как мап-файлы не имеют отличия.
Но в целом все равно этот механизм очень сложный, то есть не стоит прям сразу на него переключаться. Сделай копию сайта и поиграйся с этим как следует.
$xpdo_meta_map['Calls']= array ( 'package' => 'Rehab', 'version' => '1.1', 'table' => 'calls', 'extends' => 'xPDOSimpleObject', // Вот здесь, разве, не нужно менять? 'fields' => ...
Можно, но не обязательно, потому что мап-описание классов modAccessibleSimpleObject и xPDOObject не отличаются. Но вообще лишним не будет. Вдруг на каком-то этапе класс modAccessibleSimpleObject и в мап-файле заимеет изменения.
Илья, пока не берись за modAccessibleSimpleObject. Я вот только сейчас победил эту тему, и смог для своего объекта прописать проверку прав. Плюс там огромные заморочки с производительностью (много запросов к БД и т.п., часть инфы в сессию пользователя сохраняется). Я несколько переиначил этот механизм под свои нужды, кеширование прикрутил. В общем, пакет когда опубликую, скачаешь, посмотришь, но тема невероятно сложная по объему и количеству сопутствующих багов. Но справедливости ради стоит заметить, что оно того стоит…
Ох блин, как всё сложно! Уже 4 раза полностью переписывал огромный комментарий с возникшими вопросами, но теперь вот окончательная версия)
Во-первых, спасибо огромное за материал! Очень ценно!
Ну и, собссна, во-вторых. Какие у меня вопросы.
Возьмём класс modResource. Есть шаблоны политик доступа: 1. ResourceTemplate с разрешениями:
add_children copy create delete list load move publish remove save steal_lock undelete unpublish view
2. И есть AdministratorTemplate с кучей разрешений, из которых нам интересны:
delete_document edit_document new_document new_document_in_root new_static_resource publish_document resource_duplicate resource_quick_create resource_quick_update resource_tree save_document tree_show_resource_ids undelete_document unpublish_document view_document
В этих списках есть как уникальные, так и политики-«синонимы»:
delete == delete_document create == new_document publish == publish_document copy == resource_duplicate save == save_document undelete == undelete_document unpublish == unpublish_document view == view_document
(при чём везде '_document', но 'resource_duplicate', очередная странность). И это при условии, что в шаблоне AdministratorTemplate так же есть простые load, list, view, create, delete, copy и т.д.
Я, конечно, понимаю, что политики из первого списка можно проверить только через modResource::checkPolicy(), а из второго списка через $modx->hasPermission(), но лично для меня ясности это нифига не вносит. 1. В чём между ними в итоге разница-то? 2. Какие политики надо назначать пользователям, для нужных разрешений? Первые? Вторые? И те и другие? Здесь у меня ступор :-)
Примеры. У modResource, как наследника modSimpleAccessibleObject, вшиты проверки на load, save и remove в соответствующие методы (например). Здесь проверяется локальная политика. Предположим, что нам надо создать документ. И по сути, логично найти проверку на create где-нибудь в modSimpleAccessibleObject::newObject(). Так ведь нифига! Проверки на create ресурсов я вообще не нашёл! (может плохо искал?). Зато в процессоре на создание ресурса проверяется глобальная new_document через $modx->hasPermission(). Как такое понять??
Дальше, предположим, что нам надо опубликовать документ. Процессор modResourcePublishProcessor наследуется от modProcessor (кстати, почему бы ему сразу не отнаследоваться от modObjectProcessor, в котором уже прописан метод для проверки для глобальных прав?). Как видно из метода modProcessor::run(), в нём сначала проверяются глобальные права (вызывается modResourcePublishProcessor::checkPermissions(), который возвращает $modx->hasPermission('publish_document')), а затем вызывается метод modResourcePublishProcessor::initialize(), который проверяет локальные права save и publish! Масло масляное? Мало того, ещё и при сохрании ресурса ещё раз проверится локальная политика save! Нафига? Нафига делать одно и тоже 2 раза? Т.е. получается, что мало дать юзеру (ну группе этих юзеров) право publish_document из шаблона политик AdministratorTemplate, надо ещё и обязательно дать ему права save и publish из шаблона ResourceTemplate?? Вот об этом я ни разу не подозревал, сколько раз права настраивал. Видать не правильно настраивал? Зачем вообще разделять тогда на глобальные и локальные?
И так во многих процессорах — проверяются глобальные разрешения, потом те же, но локальные. Почему тогда не сделать все проверки глобальными и через процессоры? Вот всего этого я не понимаю :-(
Изучая процессор resource/duplicate, я начал смутно представлять зачем нужны локальные политики — там внутри методов процессора есть проверки разрешения на добавление к родительскому ресурсу дочерних документов, т.е. как-то так:
if ($parentResource->checkPolicy()){ // ... }
Да, здесь это, безусловно, оправдано. Но, исходя из вышеизложенного, картинка у меня всё-равно как-то не складывается.
Николай, помоги советом! А то я уже близок к тому, чтоб плюнуть на всё это и загулять))
И ещё пара моментов.
Просто у меня ситуация такая — есть свои классы и задумал я тут прикрутить к ним процессоры. Проблема не в создании процессоров, а в организации прав. Чего я хочу. Я хочу, чтобы юзеры с фронта могли просматривать/создавать/редактировать/удалять/getlist своих/чужих этих самых объектов. И чтобы все разрешения можно было гибко настроить из админки. Какие есть варианты? а) Создать свои глобальные политики доступа с именами вроде: 'load_my_super_object', 'list_my_super_object', 'view_my_super_object', 'create_my_super_object', 'delete_my_super_object' и т.п. Т.е. какие процессоры нужны, такие политики и насоздавать. И в каждом из этих процессоров проверять нужную в методе mod<какой-то там>Processor::checkPermission().
б) Отнаследоваться от modAccessibleSimpleObject, чтобы из коробки получить стандартные проверки на delete, save, load. Но здесь я нифига не понял — что именно нужно писать в findPolicy() и checkPolicy()?.. Всё остальное делать в процессорах. Ну и всё так же создать шаблон с вышеописанными политиками.
А ведь ещё как-то надо организовать, чтобы политиками можно было регулировать возможность просмотра/редактирования/удаления/etc. чужих объектов, т.е. созданными другими юзерами. Как такое провернуть? Надо так же создать несколько политик, вроде такого: 'load_another_my_super_object', 'list_another_my_super_object', 'view_another_my_super_object', 'delete_another_my_super_object' И в каждом процессоре на каждый чих делать проверки — нужной «another» политики и является ли юзер «владельцем» этого объекта? Или есть пути попроще?
Решил начать с создания шаблона политик. И вот здесь я снова в тупике.
Дело в том, что, при создании своего шаблона политик, modx мне предлагает в принудительном порядке выбрать на основании какого шаблона политик доступа делать новый. (нда, «политики»-«шаблоны», «политики»-«шаблоны»..)) ? Какой бы шаблон не был выбран в качестве основы, при создании новой политики — всё-равно в подсказках будет отображаться весь список всех возможных политик из всех возможных шаблонов. ? Зачем тогда давать выбирать основу? Что вообще это даёт? Я понимаю, если бы политики из этого шаблона-основы были бы доступны в созданном шаблоне, так ведь нет же! Создаётся пустой список.
И какой всё-таки надо выбирать? Есть несколько кандидатов:
ElementTemplate: add_children, copy, create, delete, list, load, remove, save, view
ObjectTemplate: list, load, remove, save, view
ResourceTemplate: add_children, copy, create, delete, list, load, move, publish, remove, save, steal_lock, undelete, unpublish, view
и AdministratorTemplate со всем его зоопарком политик.
Вот столько у меня вопросов… Извини за назойливость, но я реально здесь нифига не понимаю :-(
В коде ошибку допустил, когда редактировал. Надо вот так:
if ($parentResource->checkPolicy('add_children')){ // ... }
Привет! Коммент реально зачетный! Сразу видно, что не 5 минут потратил на изучение этого вопроса. Но наверно правильней было сразу в отдельный топик оформить. Ну да ладно, пусть здесь будет. Итак, по сабжу: так много писать не буду, а выделю основное. Ты много раз говоришь про локальные и глобальные политики, но никак не можешь уловить разницу между ними. А между тем, суть как раз и кроется в том, что означают эти названия. Глобальные — они и в Африке глобальные. И локальные там же. Вот у тебя есть глобальное право сохранять документы. ОК, априори ты можешь взять, и сохранить документ. НО, документ может находиться в той группе документов, на которую у тебя нет прав на сохранение. То есть, в принципе ты можешь сохранять их, но конкретный документ, находящийся в конкретной группе, ты сохранить не можешь — нет прав на это. То же самое и с публикацией и т.п. Это все равно, как есть у тебя глобальное право Копать. В принципе ты можешь пойти и где-то начать копать. Но придешь в городской парк, и скорее всего тебя быстро скрутят, так как конкретно в этом парке ты копать не имеешь права. Надеюсь ответил на твой вопрос.
По поводу расширения не тех процессоров, лишних проверок и т.п.: безусловно, где-то не все оптимально. Но по большей степени там все оправдано.
И еще вот такой момент по поводу проверок прав до и после сохранения. Как я и писал выше, проверки прав на документы (ресурсы), основаны на участии документов в группах ресурсов. А так как документ, пока не сохранен, по сути не может и в группе находиться, то прав локальных как бы и не имеется. И получается, что после сохранения и возможного попадения документа в какую-либо группу ресурсов, могут измениться локальные политики конкретно на этот документ.
По поводу проверки прав и наиболее удобного способа: вот это очень и очень сложный момент. Можно даже почитать статью, которая была почти сразу написана после этой статьи, чтобы оценить это примерно. Так вот, в метод checkPolicy лучше не лезть вообще. Нет, в упрощенном варианте конечно можно по какому-либо условию сделать свою проверку, и либо сразу вернуть четкий результат, либо отдать дальше на выполнение родителю через return parent::checkPolicy(), но лучше все-таки этого не делать. Если у тебя все это дело будет использоваться во фронте, то лучше все это делать именно на процессорах, так как тебе довольно не сложно будет расширить базовые процессоры и просто в своих выполнить проверку на уровне нативных методов. Это получится дешево и сердито.
Я хочу, чтобы юзеры с фронта могли просматривать/создавать/редактировать/удалять/getlist своих/чужих
Вот это тоже очень интересный момент. Я не раз говорил (и Джейсону тоже), что у нас с политиками есть очень большой промах. У нас в принципе нет такого понятия сейчас как владелец. В моем понимании все создаваемые объекты должны иметь флаг владельца, чтобы можно было выставить исключительные права. Второй момент (тоже очень досадный), что все рассчитано на групповые политики. То есть, права выдаются на группы пользователей, но не выдаются на конкретного пользователя. В итоге получается, что чтобы дать индивидуальные права одному пользователю, надо создать для него группу и отнести его к этой группе. В итоге 1000 пользователей — 1000 групп. Вообще не кульно получается. В своем ModZilla я расширил этот механизм и добавил механизм индивидуальных прав для пользователей, но во-первых, там все равно все не идеально, во-вторых, это вряд ли будет включено в официальное ядро в обозримом будущем, а в третьих, это вообще становится все сложно :-)
По поводу шаблонов и общего списка: шаблоны созданы не для того, чтобы сделать исключительно индивидуальные списки, а чтобы разделить единый список. Ведь в конкретной политике на основе конкретного шаблона имеется ограниченное количество вариантов. Согласись, гораздо удобней видеть, что в этой политике назначено 7 из 15-ти, чем просто 7 из вообще всего набора прав. Это если ты пользователям всего одну роль создаешь, это может особо и не понятно, но когда у тебя на сайте штук 30 политик, тогда ты это все оценишь.
Сразу видно, что не 5 минут потратил на изучение этого вопроса.
Даа, не один час ушёл)) И даже не три)
еще вот такой момент по поводу проверок прав до и после сохранения. Как я и писал выше...
А, блин! Вот теперь дошло, почему я ни где локальной проверки на create не нашёл.
Блин, а можно в скайп голосом вопрос задать? А то, если я сейчас свой вопрос печатать буду, то ещё 2 часа уйдёт(
У меня не много, просто печатать это сложно и долго.
А аналогия с копанием порвала! Спасибо, стало понятнее))
В скайпе у меня такса 1000 рублей/час. Сорри, но со временем у меня напряг. Здесь, в паблике, вы платите уникальным контентом. А в привате для меня все в минус.
Я, кстати, полностью разобрался в вопросе. Прям вот полностью)
Сорри, но со временем у меня напряг
Нельзя ж постоянно работать!))
Нельзя. Но когда я не работаю, я лучше просто погулять пойду. А консультации — это тоже работа, и бесплатно ее делать не улыбается.
Не, ничего не подумай, я с этим согласен полностью! Сам так же поступаю)

Добавить комментарий