Drupal 7: Программно контролируем доступ к материалам

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

Но что если нам нужно лишь блокировать доступ определенным ролям? Допустим, мы добавили к типу содержимого Article поле “Для своих”, которое является обычным чекбоксом, а также создали роль пользователей, которые являются “своими”. А теперь нам необходимо сделать так, чтобы эту запись видели только “свои” когда у материала поставлена галочка “для своих”, а другие бы не могли обратиться к данному содержимому не имея соответствующих прав. Что делать в таком случае?

Первым на ум приходят модули с drupal.org, но я расскажу как это делается самостоятельно, написав небольшой модуль.

Подготовка сайта

Это по сути пошаговый туториал для начинающих. Если вы хорошо разбираетесь в коде, то и воссоздавать среду для тестов не придётся.

Я не особо хочу заморачиваться с подготовкой сайта, поэтому всё очень просто:

  1. Устанавливаем Drupal с профилем Standart.
  2. Типу содержимого Article добавляем поле: 1. Название: “Для своих”;
  3. Машинное имя: field_members_only;
  4. Тип: boolean;
  5. Виджет: Single on\off checkbox.
  6. Добавляем новую роль для пользователей: Members и располагаем после авторизованных пользователей.
  1. Скачайте и расположите заготовку для модуля в /sites/all/modules/.

Теоретическая часть

Все крутится вокруг двух хуков hook_node_grants() и hook_node_access_records().

hook_node_grants()

Данный хук отвечает за выдачу прав пользователю. При просмотре ноды, пользователь в зависимости от тех условий что мы задаем, получит определенный уровень доступа к содержимому. Т.е. в данном хуке мы определим уровни доступа.

hook_node_access_records()

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

Практическая часть

Собственно код со всеми комментариями.

/**
 * @file
 * Здесь мы будем писать весь код.
 */

/**
 * Для начала объявим константы для более удобного контроля доступа.
 * Так как он весь завязан на цифрах от 0 и выше, то чтобы не запутаться, проще
 * вынести их в константы. Так будет намного читабельнее и яснее что делается.
 *
 * MYMODULE_ACCESS_REALM - название нашего "реалма" внутри которого будут
 * выдаваться права. Это что-то вроде машинного имени для наших уровней доступа
 * внутри которого отрабатывают наши условия.
 *
 * А также объявляем две константы с уровнем доступа:
 *   - MYMODULE_ACCESS_PUBLIC: которая равняется нулю, что в свою очередь
 *     является уровнем доступа для просмотра публичных материалов.
 *   - MYMODULE_ACCESS_PRIVATE: равняется единице, что будет соответстовать
 *     праву на просмотр скрытого содержимого.
 *
 * Цифры могут быть любыми, задаются на усмотрение. Но для понимания мы делаем
 * их в порядке увеличения. Чем выше цифра - тем больше прав.
 */
define('MYMODULE_ACCESS_REALM', 'mymodule_access_article');
define('MYMODULE_ACCESS_PUBLIC', 0);
define('MYMODULE_ACCESS_PRIVATE', 1);

/**
 * Используем hook_node_grants().
 *
 * Данный хук срабатывает при просмотре содержимого и выдаёт пользователю
 * соответствующий уровень доступа к содержимому.
 *
 * $account - информация о пользователе, который обратился к ноде.
 * $op - операция которая выполняется (view, edit, delete).
 */
function mymodule_node_grants($account, $op) {
  // Нас интересует лишь просмотр содержимого. Поэтому права мы выдаем именно
  // в момент просмотра содержимого. Редактирование и удаление будет ограничено
  // системными правами (что в админке друпала).
  if ($op == 'view') {

    // Теперь мы проверяем, имеет ли текущий пользователь роль 'Members'.
    // Т.е. условие может быть каким угодно, но в нашем случае, мы определяем
    // будет ли иметь доступ по роли.
    if (in_array('Members', $account->roles)) {
      // Наш пользователь имеет роль 'Members' и мы выдаем ему права на
      // просмотр публичного И приватного содержимого.
      // Если указать только права на приватное содержимое, то пользователь
      // не сможет увидеть публичное.
      $grants[MYMODULE_ACCESS_REALM] = array(
        MYMODULE_ACCESS_PUBLIC,
        MYMODULE_ACCESS_PRIVATE,
      );
    }
    else {
      // Ну а если у пользователя нету роли 'Members' то мы разрешаем смотреть
      // только публичные материалы.
      $grants[MYMODULE_ACCESS_REALM] = array(
        MYMODULE_ACCESS_PUBLIC,
      );
    }

    return $grants;
  }
}

/**
 * Используем hook_node_access_records().
 *
 * В данном хуке определяется, какой уровень доступа необходим для ноды.
 * Данная запись делется при редактировании\добавлении нового материала.
 *
 * Если у вас уже есть содержимое, которому нужно "пересобрать" права, то
 * воспользуйтесь фукнцией node_access_rebuild() или в админке:
 * admin/reports/status/rebuild
 */
function mymodule_node_access_records($node) {

  // Мы задаем права доступа только для нашего типа содержимого 'Article'.
  if ($node->type == 'article') {
    // Получаем значения поля "Для своих".
    $members_only = field_get_items('node', $node, 'field_members_only');

    // Если отмечено "Для своих".
    if ($members_only[0]['value']) {
      // Указываем ноде, что смотреть её могут пользователи только с gid
      // который отвечает за просмотр приватного содержимого.
      // Также обратите внимание что у обновления и удаления у нас стоят нули
      // так как мы выдаём лишь на просмотр.
      $grants[] = array(
        'realm' => MYMODULE_ACCESS_REALM,
        'gid' => MYMODULE_ACCESS_PRIVATE,
        'grant_view' => 1,
        'grant_update' => 0,
        'grant_delete' => 0,
        'priority' => 0,
      );
    }
    else {
      // Если не отмечено "Для своих", то мы открываем материал всем желающим.
      $grants[] = array(
        'realm' => MYMODULE_ACCESS_REALM,
        'gid' => MYMODULE_ACCESS_PUBLIC,
        'grant_view' => 1,
        'grant_update' => 0,
        'grant_delete' => 0,
        'priority' => 0,
      );
    }
  }

  return $grants;
}

Нюансы

Имейте ввиду, что главный админ сайта будет обходить все эти проверки. Так что дебажить эти хуки не получится при помощи devel. Либо создайте пользователя с доступом к девелу, либо включите специальный для этого модуль Devel node access и вынесите соответствующий блок в видный регион. Он будет красиво, в табличке показывать, кто что может, а кто нет.

Пояснения

Чтобы ещё больше внести ясности, вот небольшое пошаговое объяснение.

  1. Создаем\редактируем ноду. После сохранения она смотрит наше поле field_members_only. Если отмечено, то для ноды устанавливается уровень доступа MYMODULE_ACCESS_PRIVATE (1), если же не отмечено, то MYMODULE_ACCESS_PUBLIC (0). Эта информация будет храниться до следующего изменения ноды, либо до пересбора всех прав доступа (admin/reports/status/rebuild).
  2. Аноним\пользователь (без роли members) открывает страницу ноды, которая помечена “Для своих” (MYMODULE_ACCESS_PRIVATE (1)). Происходит проверка. Так как роли Members у них нету, им выдается уровень доступа MYMODULE_ACCESS_PUBLIC (0). Так как нода приватная и требует уровень 1, то доступ к содержимому будет закрыт и будет показана страница 403 (access denied).
  3. Пользователь с ролью Members открывает страницу с нодой, которая помечена “Для своих”. В результате проверки, он получает права 0 и 1. Так как нода требует уровень 1 и пользователь его имеет, он увидит её содержимое.

В чём плюсы

  1. В отличии от блокировки доступа средствами Rules, текущий вариант железный. Нету роли - нету доступа.
  2. Можно гнуть как угодно. Любые условия, проверки и т.д. На что хватит фантазии.
  3. В результатах Views не будут отображаться ноды, доступа к которым не имеет пользователь. Их увидят только те, кто подходит под условия.

Всё не так уж и сложно. Главное уловить идею про выдачу уровней доступа и понимание придёт. Удачи в создании закрытых разделов ;)

Прикрепленные файлы

Оставайся на связи

Будь всегда в курсе новых материалов! Подписывайся на каналы\паблики\рассылки чтобы получать оперативные оповещения.

DrupalCamp Краснодар 2017

DrupalCamp — важная часть жизни Друпал-сообщества, способствующая его сплочению, привлекающая внимание IT-сообщества к Друпалу и общества в целом к разработке приложений и сайтов, а также к смежным сферам.

Разработчики, веб-мастера, дизайнеры, менеджеры проектов, владельцы бизнеса и работодатели — внимание большого количества людей будет привлечено нашим событием.

Интересно будет всем, ждем вас 16 декабря в Краснодаре!

Официальный сайт мероприятия

Комментарии

d
drupby 22.06.2014 - 09:06

в данном случае неплохо справился бы и hook_node_access, надо было бы в статье написать про разницу с ним. к примеру при использовании hook_node_access доступа к странице не будет, но вьюха будет выводить все ноды

Ответ на от drupby (не проверено)

N
Niklan 22.06.2014 - 10:06

Да, тизеры будут выводиться во вьюхах и т.д. В случае использования hook_node_access.

В варианте из hook_node_grants() и hook_node_access_records() получается намного "защищёнее" и гибче. Просто стояла задача сделать закрытые разделы и некоторые типы содержимого позволить скрывать "для своих" и hook_node_access() сразу отпал, когда в общем списке новостей выводились тизеры "скрытых от глаз" новостей. Это очень важная часть для ограничения доступа к ноде. Что же это за скрытая нода которая во вьюхе спокойно выводится :)

К
Кирилл 15.01.2015 - 16:01

Спасибо, отличная статья. Внедрил у себя, но на других (не на тех, для которых ограничиваем доступ) материал при редактировании стал выскакивать
Notice: Undefined variable: grants in mymodule_node_access_records() (line 148 of sites/all/modules/mymodule/mymodule.module).
Наверное, надо переменной в начале функции присвоить какое-то дефолтное значение?

Ответ на от Niklan

К
Кирилл 15.01.2015 - 19:01

Скопировал Ваш код, только поменял название типа материала и название роли. Т.е. да, проверка есть:

function mymodule_node_access_records($node) {

  // Мы задаем права доступа только для нашего типа содержимого 'Basic page'.
  if ($node->type == 'page') {

Ответ на от Кирилл (не проверено)

N
Niklan 15.01.2015 - 19:01

В фукнции 

function mymodule_node_access_records($node) {

return $grants находится за пределами условия, внесите его туда.

Ответ на от Niklan

К
Кирилл 16.01.2015 - 13:01

Перенёс на одну фигурную скобку выше, сейчас вроде всё ок. 

И
Илья 20.07.2015 - 15:07

Спасибо! Очень хорошо рассказано!

О
Олег 11.03.2016 - 18:03

Никита, все круто с блогом. Но есть одно как по мне существенное замечание. Не нужно использовать русский язык в комментариях по коду. Это нарушает код стандарты. (Представь что тебе попадается код на смеси языков например каталонский испанский английский) Это приучает молодежь к нехорошим практикам. Если хочется что-то описать на русском как по мне есть тест статьи.

Ответ на от Олег (не проверено)

А
Алексей 15.08.2017 - 18:08

Ну блог то русский, читатели русские, думаем мы по русски, комментарии нужно делать на арабском?

Д
Дмитрий 04.11.2017 - 23:11

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

Ответ на от Дмитрий (не проверено)

N
Niklan 15.11.2017 - 12:11

Вам как-то надо это всё заложить в логику данных хуков, иначе никак. Эти хуки режут везде, и во вьюсах и на прямые запросы. Просто надо посидеть и подумать как это сделать, так как согласен, не совсем логичная система, но она достаточно гибкая, считаю, что ваши нужды решит. Можете uid юзеров юзать как гранты или ещё чего. Или связки nid + uid генерить, а потом по ним открывать доступ.

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

Содержимое данного поля является приватным и не предназначено для показа.