$xpdo_meta_map['Calls']= array ( 'package' => 'Rehab', 'version' => '1.1', 'table' => 'calls', 'extends' => 'xPDOSimpleObject', // Вот здесь, разве, не нужно менять? 'fields' => ...
ОК. Кстати, я сейчас тикетную систему прям сюда разрабатываю, будет вообще ураган. И для полного счастья останется только здесь же облако свое сделать :-) Чтобы политики доступов пользователя учитывались в доступах к самому сайту-проекту.
Ну мой график ты знаешь — если проекта нет, то по будням 3 часа могу уделять и аудиту)))
А чтобы свой объект сделать объектом modAccessibleSimpleObject (сейчас он xPDOSimpleObject), достаточно переписать тип объекта в мап-файле и в самом классе объекта?
Достаточно просто класс расширить modAccessibleSimpleObject, так как мап-файлы не имеют отличия.
Но в целом все равно этот механизм очень сложный, то есть не стоит прям сразу на него переключаться. Сделай копию сайта и поиграйся с этим как следует.
А чтобы свой объект сделать объектом modAccessibleSimpleObject (сейчас он xPDOSimpleObject), достаточно переписать тип объекта в мап-файле и в самом классе объекта?
Просто сейчас для проверки доступа сам прописал метод checkAccess (чтобы не путаться с checkPolicy — так как объект другой) и в процессоре приходится в метод beforeSet добавлять $this->object->checkAccess().
Было бы хорошо просто в объекте расширить метод checkPolicy и процессор оставить обычный, просто расширяющий modObjectUpdateProcessor.
Продолжение недавней статьи о доступах к объектам. Там я писал:
Сразу отмечу, что большинство используемых методов рассчитаны только на работу уже с конечными объектами (что может повлиять на разницу подсчета строк в базе данных, и конечным числом полученных объектов). То есть при выборке записей из базы данных xPDO не формирует автоматически запрос так, чтобы исключить записи для тех объектов, к которым у пользователя нет доступов. xPDO сделает выборку всех записей, и только потом для каждой записи он постарается получить инстанс объекта в методе xPDOObject::_loadInstance().
Как оказалось, в этом моменте крылся глобальный пробел в системе MODX в плане выборки защищенных объектов. Попробую объяснить… Представьте, что у вас на сайте 10000 документов. 15 документов из этих 10000 относятся к приватным группам ресурсов, то есть должны быть доступны только определенным пользователям. Казалось бы, наша задача — просто сделать выборку этих 15-ти документов и все. И вот здесь epic fail… В xPDO в методах выборки getCollection() и т.п. не прописываются автоматом условия проверки на право load объекта. То есть xPDO сделает выборку всех записей, в потом по каждой из них попытается создать новый объект, и уже в этот момент он будет пытаться проверить на право доступа к объекту. Вот такой невеселый момент… Есть еще пара неприятностей, но это уже не так принципиально. Самый пробел это именно с выборками, так как, к примеру, процессор подсчитал, что в БД есть 500 записей, удовлетворяющих условию, выбрал по лимиту 20 записей из БД, и когда попытался проинициировать конечные объекты, оказалось, что пользователь не имеет к ним доступа. И итоговая информация следующая: «всего 500 записей, но ничего не найдено для вас», хотя где-то в глубине таблицы есть записи объектов, к которым пользователь доступ имеет, просто ему не повезло с выборкой.
В общем все это печально, но не безнадежно. У нас же ООП :-) И если учесть, что мы не используем все методы работы с xPDO-объектами, то можно просто перегрузить некоторые методы, к примеру load, getCollection() и т.п., и в них добавить индивидуальные условия для SQL-запросов. Но надо учитывать то, что в большинстве случаев MODx оперирует с JSON-массивом наборов политик в таблице modx_access_policies, а это нам не позволит делать выборки на уровне SQL. В общем, сейчас ищу пути решения…
UPD: В общем я пришел к вот такому интересному способу: Проблема уже была озвучена чуть выше — настройки прав доступов хранятся в JSON-формате, и для выборки они нам вообще бесполезны. Но для тех классов, число записей для которых предполагается очень большое количество, у меня собственная таблица записей прав доступов. И так как эта таблица содержит записи только для моих классов, а на уровне выборки нас интересует только право «load» (то есть право получать инстанс объекта), то сам факт наличия записи в этой таблице уже с большой долей вероятности сигнализирует о том, что по объекту необходимо будет проверить права. А если для конкретного объекта нет записей в этой таблице, значит для него действует правило «не запрещено — значит разрешено всем». В таком случае нас интересует только три ситуации:
  1. Для конкретного объекта не существует записи в таблице прав доступов.
  2. Запись есть, но для нее есть настройки прав текущего пользователя.
  3. Запись есть, но для нее есть настройки прав групп пользователей текущего пользователя.
Кстати, да — в моей модели к элементам будут настраиваться как доступы для отдельных пользователей, так и для групп пользователей. Это очень важный момент. К тем же группам ресурсов нельзя дать доступ конкретному пользователю. Можно только создать группу пользователей и дать доступ для группы пользователей с какой-либо ролью, и чтобы конкретный пользователь получил доступ, его надо отнести к этой группе пользователей. И получается, что если 1000 пользователей захотят дать доступ другой 1000 пользователей к каким-то уникальным элементам, надо будет создать 1000 групп пользователей. Вообще не айс. А так можно или дать доступ группе пользователей (и все пользователи этой роли получат разрешенные доступы к этому элементу, если роль позволяет), или можно просто дать доступ пользователю (опять же с указанием роли), и тогда уже будут проверяться доступы конкретного пользователя. И вот это вообще ураган! К примеру, есть крупный проект, над которым предполагается командная работа нескольких специалистов (дизайнер, верстальщик, программист и т.п.). Для удобства можно создать группу проекта, и занести всех пользователей в эту группу. Но с разными ролями (пусть это так же будет дизайнер, верстальщик, программист и т.п.). Так вот, допустим, все эти пользователи из группы будут иметь права на просмотр тикетов (ведь вроде все в одной коляске), и еще у них будет собственная закрытая ветка на форуме. И все, кто в группе, те имеют эти доступы (доступ группы). При этом программисты имеют еще какие-то права, и верстальщики. Но решили взять еще одного верстальщика. И вот хотя для него роль «верстальщик» лучше всего подходит (у него есть доступ к спецресурсам для верстальщиков), доступ к своей ветке форума ему не хочется давать (это только для совсем своих). Так вот, можно его не в группу с ролью «верстальщик» добавить, а просто дать доступ как для отдельного пользователя с ролью верстальщика.
Но для того, чтобы эта выборка работала правильно, мне надо добавлять определенные условия в выборку. Можно было бы конечно перегрузить метод loadCollection(), но по ряду причин я не стал этого делать. Вместо это я создал статический метод addCheckListConditions в самом классе ModzillaAccess, так как все мои классы в большинстве случаев для хранения настроек доступов будут использовать именно этот его. Вот код:
public static function addCheckListConditions(xPDO & $xpdo, xPDOCriteria $criteria){ if(empty($criteria->hasCheckListConditions)){ if(!$xpdo->user->get('sudo')){ $criteria->leftJoin('ModzillaAccess', 'Accesses'); $criteria->leftJoin('modUserGroupMember', 'membergroup', "Accesses.principal_class='modUserGroup' AND Accesses.principal = membergroup.user_group"); $criteria->where(array( 'Accesses.id' => null, )); $criteria->orCondition(array( "Accesses.principal_class" => "modUser", "AND:Accesses.principal:=" => $xpdo->user->get('id'), )); } $criteria->hasCheckListConditions = true; } return $criteria; }
Тонкости используемых здесь условий писал в прошлом топике.
Вот теперь если я хочу загрузить коллекцию объектов ModzillaProject с учетом прав ModzillaAccess, я делаю так:
// Создаем новый запрос $q = $modx->newQuery('ModzillaProject'); // Добавляем правила проверки в запрос ModzillaAccess::addCheckListConditions($modx, $q); // Если хотим, считаем количество записей $total = $modx->getCount('ModzillaProject', $q); // Получаем коллекцию объектов $projects = $modx->getCollection('ModzillaProject', $q);
И все.
Но еще вопрос: все ли на этом? Это все проверки? Нет, не все :-) Как говорится «стрижка только началась»… Напоминаю, что в методе modAccessibleObject::loadCollection() сначала происходит выборка записей из БД, потом для каждой записи выполняется попытка создания объекта с проверкой прав пользователя на этот объект. Записи с фильтром мы выбрали. Теперь нам еще предстоит выполнить проверку прав на создание конечных объектов. Этот механизм описывался здесь: modxclub.ru/blog/dokumentatsiya-dlya-spetsialistov/26.html
У нас это не ломается. То есть нам предстоит только прописать метод поиска настроек доступов в методе findPolicy();
Для знатоков SQL-я.
Мне панадобилась хитрая выборка WHERE condition AND (condition2 OR condition3). Обратите внимание, что здесь принципиальная разница по сравнению с WHERE condition AND condition2 OR condition3, так как во втором случае в результат попадут записи по условию OR (третьего условия) независимо от успеха первого и второго условия. А так получается, что первое условие должно выполниться обязательно, а второе или третье — как получится (но обязательно хотя бы или первое, или второе). И само собой это должно быть на xPDO.
Итак, запрос:
$uid = $modx->user->get('id'); $q = $modx->newQuery('ModzillaAccess'); $q->leftJoin('modUserGroupMember', 'membergroup', "ModzillaAccess.principal_class='modUserGroup' AND ModzillaAccess.principal = membergroup.user_group"); $q->leftJoin('modAccessPolicy', 'policy', "ModzillaAccess.policy=policy.id"); $q->select(array( "ModzillaAccess.*", "policy.data as acl", )); $q->where(array( "target" => 1, )); $q->andCondition(array( "principal_class" => 'modUser', "AND:ModzillaAccess.principal:=" => $uid, )); $q->orCondition(array( "principal_class" => 'modUserGroup', "AND:membergroup.member:=" => $uid, ));
То есть здесь выборка делается или по пользователю, или по группе пользователя.
Сразу опишу тему топика. В топике поднимаются вопросы по уровням политик доступов в 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;
У кого какие вопросы, обязательно спрашивайте, так как этот материал надо понимать досконально. Считайте его тестером ваших знаний. Если вам здесь что-то не понятно, значит еще обязательно надо заполнять пробелы в знаниях.
Антон, привет!
1. Сообщество должно быть закрытым
Так а у нас разве не так? У нас зарегистрироваться может всякий, но далеко не все пользователь видит. Для того, чтобы увидеть все, пользователь должен быть повышен в правах.
2. Надо самим искать новичков и приглашать их.
С одной стороны да. Но здесь и другой еще путь есть: сайт открыт для регистрации, и как и на хабре, пользователь может себя проявить в ответах и т.п., показать свой профессионализм, и его всегда можно подтянуть в команду.
3. И самое главное! Нужен хороший руководитель-менеджер, без него никуда.
Это однозначно. Конечно надеюсь, что у нас здесь будут грамотные менеджеры проектов.
Сергей, привет! Рад что мои идеи нашли понимание и среди других членов клуба. И полностью согласен со всем, что ты сказал. Собственно, так все и задумывалось.
По поводу фриланса: все верно. Безусловно фриланс (свободная занятость и заработок) — это основная идея клуба, без этого просто никак. Но все остальное мне кажется довольно оригинальной идеей, родившейся на основе анализа рынка. Я не собираюсь здесь делать новый free-lance.ru, но я собираюсь дать то, чего не дает free-lance.ru и еже с ним. В частности, четкий контроль качества и гарантию выполнения проекта. К примеру, тот же free-lance.ru имеет такой инструмент гарантии, как «сделка без риска». Но что он гарантирует? Он только гарантирует то, что деньги заказчика не потеряются, в случае ненадлежащего исполнения обязательств исполнителя, а исполнителю гарантируется, что если проект выполнен, он получит свои деньги. Но free-lance.ru не гарантирует заказчику выполнение проекта. Если проект не будет выполнен, заказчик не потеряет деньги, но и время он не вернет, и выполненного проекта у него не будет. В MODX-клубе же все проекты будут выполнены в обязательном порядке (если они проходят через сам MODX-клуб). Если не справится специалист, который взялся за проект, этот проект на выполнение заберут более опытные специалисты. В любом случае система будет выстраиваться так, чтобы и новички могли руку набивать, и чтобы проекты не страдали и обязательно выполнялись. Второе отличие: ориентированность на личность. Я хочу, чтобы многие друг друга по имени знали, знали какие есть способности и т.п. Какая-то статическая информация по навыкам и т.п. будет просто видна в профиле, но и сами участники клуба должны друг с другом более плотно взаимодействовать. Помогать советом, участвовать в общих проектах и т.п. Это курс на качество и новый уровень. Здесь вряд ли будет такая огромная движуха, как на free-lance.ru, но здесь точно будет качество и лучшие технологии. Этот как автомобили бентли. Их просто физически нельзя клепать столько же, сколько жигулей клепается. Но многие согласятся, что один бентли стоит тысячи жигулей.
И да, ваша помощь а развитии проекта однозначно понадобится, без этого просто никак. По мере необходимости я буду объявлять о задачах, но в дальнейшем работа будет строиться по принципу пирамиды, то есть не на мне все будет зациклено, но и ТОПовые участники клуба постепенно встанут во главе проекта. Каждый себе постепенно наберет команду помощников, те себе новичков наберут и т.п. Чтобы не было беспрезорников, но и была какая-то вертикаль взаимодействия, так как если здесь будет 1000 и более членов клуба, то при всем желании нельзя будет отвечать всем и каждому. Но каждый должен знать, куда ему обратиться, чтобы он точно получил ответ. И если тот, кому он задал ответ, не будет знать на него ответ, он обратится дальше. Не думаю, что здесь возникнут тупиковые вопросы.