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

22.06.2014

Бывают такие задачи, где есть необходимость, создать скрытый от пользователей контент. Допустим, у нас есть тип материала 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 и вынесите соответствующий блок в видный регион. Он будет красиво, в табличке показывать, кто что может, а кто нет.

Devel node access
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 не будут отображаться ноды, доступа к которым не имеет пользователь. Их увидят только те, кто подходит под условия.

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

Комментарии

d
drupby
22.06.2014

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

N
Niklan
22.06.2014

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

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

К
Кирилл
15.01.2015

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

N
Niklan
15.01.2015

Вы уверены что в коде есть условие на проверку типа содержимого?

К
Кирилл
15.01.2015

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

function mymodule_node_access_records($node) {

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

N
Niklan
15.01.2015

В фукнции 

function mymodule_node_access_records($node) {

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

К
Кирилл
16.01.2015

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

И
Илья
20.07.2015

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

О
Олег
11.03.2016

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

А
Алексей
15.08.2017

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

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