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

Берем полный контроль над доступом к материалу.

22.06.2014
13 комментариев
5 мин.

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

Пояснения

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

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

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

Прикрепленные файлы
Заготовка модуля — mymodule_empty.tar.gz, 340 байт
Готовый модуль — mymodule_done.tar.gz, 2.28 КБ
Drupal
Drupal 7
Access

Комментарии

drupby   вс, 22/06/2014 - 09:59

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

Niklan   вс, 22/06/2014 - 10:02

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

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

Кирилл   чт, 15/01/2015 - 16:33

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

Niklan   чт, 15/01/2015 - 16:48

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

Кирилл   чт, 15/01/2015 - 19:04

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

function mymodule_node_access_records($node) {

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

Niklan   чт, 15/01/2015 - 19:37

В фукнции 

function mymodule_node_access_records($node) {

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

Кирилл   пт, 16/01/2015 - 13:05

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

Олег   пт, 11/03/2016 - 18:45

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

Алексей   вт, 15/08/2017 - 18:57

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

Дмитрий   сб, 04/11/2017 - 23:32

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

Niklan   ср, 15/11/2017 - 12:02

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

Roman   сб, 07/04/2018 - 07:32

Прикрепленные к статье файлы заготовок и готового модуля недоступны почему-то для скачивания (error 403) :(