Fi1osof 28 сентября 2014 0 0
Материал для экспертов.

В одном из топиков я уже писал довольно подробно про политики безопасности в MODX. Тогда мы разобрались, что основной метод проверки — modAccessibleObject::checkPolicy(). В этот раз мы попробуем более внимательно изучить этот механизм, и рассмотрим очень важную недоработку MODX в этом плане.

Во-первых, сразу хочу отметить, что метод checkPolicy не содержится в исходных классах xPDO типа xPDOObject или xPDOSimpleObject. Этот метод появляется только в MODX-классе modAccessibleObject, который входит в сам пакет modx (да, не забываем, что MODX — это надстройка над xPDO и подключается как дополнительный пакет). То есть, если вы хотите использовать чистый xPDO, то имейте ввиду, что механизм политик безопасности там попросту отсутствует.

А во-вторых, рассмотрим важную недоработку: отсутствие возможности выполнить проверку доступов к объекту сразу для нескольких пользователей.
Простой пример: вот у нас здесь на сайте есть закрытые блоги, к которым имеют доступы только определенные группы пользователей. Встала задача — при публикации нового топика в блог, разослать уведомления всем пользователям, которые имеют доступ к блогу, в который публикуется топик. И вот вопрос: как это сделать? По логике, нам надо получить всех пользователей, перебрать их в цикле и каждого из них проверить на предмет доступа к объекту блога. Это по логике. А на практике? А на практике метод modAccessibleObject::checkPolicy() принимает только два параметра — $criteria и $targets. А внутри метода прописана работа с текущим пользователем MODX $this->xpdo->user. Таким образом проверять права мы можем только для текущего пользователя :)
Хотя можно было бы конечно попробовать что-то вроде такого изврата:
<?php
$modx->switchContext('web');
$blog_id = 1; // ID нужного блога
$currentUser = $modx->user; 
$blog = $modx->getObject('SocietyBlog', $blog_id);
foreach($modx->getCollection('modUser' ) as $user){
    $modx->user = $user;
    print "\n<br />HasAccess: ". (int)$blog->checkPolicy('view');
}
$modx->user = $currentUser; 

То есть загоняем текущего MODX-пользователя в переменную, затем в цикле перегружаем текущего пользователя и проверяем доступ к объекту, после чего уже возвращаем текущего пользователя в объект $modx. Но это конечно же не по фэншую. Хотя просто так альтернативы нет…

В нашем же случае с топиками и блогами есть вариант — это немного переписать метод checkPolicy() в наших пользовательских классах, добавив третий параметр — $user. К слову, пуллреквест в MODX я уже отправил (смотрите изменения в коде).
В результате все получается так, как и должно быть:
<?php
$modx->switchContext('web');
$blog_id = 1; // ID нужного блога
$blog = $modx->getObject('SocietyBlog', $blog_id);
foreach($modx->getCollection('modUser' ) as $user){
    print "\n<br />HasAccess: ". (int)$blog->checkPolicy('view', null, $user);
}
Без всякой перегрузки текущего MODX-пользователя. Будем надеяться, что пуллреквест будет приниматься не особо долго.

По поводу производительности: на холодную перебор 500 пользователей на предмет доступа к объекту на моем сервере занимает примерно 4-5 секунд. Далее уже значительно быстрее, в районе 0.2-0.3 сек. Здесь играет роль внутренний механизм кеширования доступов для каждого пользователя в отдельности, заложенный в MODX-е, но изучать детально я его буду чуть позже. Тогда и статью по этому поводу напишу отдельно. Опыт подсказывает, что там скрываются очень полезные механизмы, которые можно будет использовать для существенного снижения нагрузки на сайтах с распределенными правами доступов.

И напоследок, приведу код метода SocietyTopic::checkPolicy(). Дело в том, что там метод checkPolicy еще чуть более измененный. Во-первых, права проверяются не только на сам топик, но и на доступы к блогам, в которых топик размещен (Да, топик может быть индивидуально ограничен в доступах. И да, топик может располагаться сразу в нескольких блогах. Тогда наличие доступа хотя бы к одному из блогов топика будет свидетельствовать о наличии прав на топик).

<?php
    public function checkPolicy($criteria, $targets = null, modUser $user = null) {
        if(!$user){
            $user = & $this->xpdo->user;
        }
        // Проверяем права на блог (хотя бы один)
        $hasBlogAccess = false;
        foreach((array)$this->TopicBlogs as $topicblog){
            // print_r($topicblog->Blog->toArray());
            if(
                $blog = $topicblog->Blog
                AND $blog->checkPolicy($criteria, $targets, $user)
            ){
                $hasBlogAccess = true;
                break;
            }
        }
        if(!$hasBlogAccess){
            return false;
        }
        if ($criteria && $this->xpdo instanceof modX && $this->xpdo->getSessionState() == modX::SESSION_STATE_INITIALIZED) {
            if ($user->get('sudo')) return true;
            if (!is_array($criteria) && is_scalar($criteria)) {
                $criteria = array("{$criteria}" => true);
            }
            $policy = $this->findPolicy();
            if (!empty($policy)) {
                // print "sdfdfd";
                $principal = $user->getAttributes($targets);
                if (!empty($principal)) {
                    foreach ($policy as $policyAccess => $access) {
                        foreach ($access as $targetId => $targetPolicy) {
                            foreach ($targetPolicy as $policyIndex => $applicablePolicy) {
                                if ($this->xpdo->getDebug() === true)
                                    $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, 'target pk='. $this->getPrimaryKey() .'; evaluating policy: ' . print_r($applicablePolicy, 1) . ' against principal for user id=' . $user->id .': ' . print_r($principal[$policyAccess], 1));
                                $principalPolicyData = array();
                                $principalAuthority = 9999;
                                if (isset($principal[$policyAccess][$targetId]) && is_array($principal[$policyAccess][$targetId])) {
                                    foreach ($principal[$policyAccess][$targetId] as $acl) {
                                        $principalAuthority = intval($acl['authority']);
                                        $principalPolicyData = $acl['policy'];
                                        $principalId = $acl['principal'];
                                        if ($applicablePolicy['principal'] == $principalId) {
                                            if ($principalAuthority <= $applicablePolicy['authority']) {
                                                if (!$applicablePolicy['policy']) {
                                                    return true;
                                                }
                                                if (empty($principalPolicyData)) $principalPolicyData = array();
                                                $matches = array_intersect_assoc($principalPolicyData, $applicablePolicy['policy']);
                                                if ($matches) {
                                                    if ($this->xpdo->getDebug() === true)
                                                        $this->xpdo->log(modX::LOG_LEVEL_DEBUG, 'Evaluating policy matches: ' . print_r($matches, 1));
                                                    $matched = array_diff_assoc($criteria, $matches);
                                                     if (empty($matched)) {
                                                        return true;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                return false;
            }
        }
        return true;
    }


UPD: В итоге я отправил пулл-реквест, который все-таки приняли, и теперь можно проверять права для любых пользователей.
0 комментариев
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.