Пример интеграции прав доступа
Статья описывает базовый сценарий интеграции прав доступа в модуль example. После настройки модуль сможет хранить роли, показывать таблицу прав через BX.UI.AccessRights и проверять доступ пользователя методом ExampleAccessController::can().
Как работает проверка доступа
Класс Bitrix\Main\Access\BaseAccessController принимает идентификатор пользователя, действие, идентификатор объекта и дополнительные параметры:
$canEdit = ExampleAccessController::can( $userId, ActionDictionary::ACTION_EDIT, $exampleId );
Метод can() приводит идентификатор объекта к int, вызывает loadItem(), создает правило по коду действия и запускает execute(). Если правило не найдено, контроллер выбрасывает UnknownActionException.
Правило ищется в пространстве имен Rule рядом с контроллером. Код действия преобразуется в имя класса: create — в CreateRule, edit — в EditRule, task_admin — в TaskAdminRule.
Создать словари действий и разрешений
Класс действий хранит коды операций, которые проверяет контроллер доступа:
<?php
namespace Bitrix\Example\Access;
final class ActionDictionary
{
public const ACTION_CREATE = 'create';
public const ACTION_EDIT = 'edit';
}
Класс разрешений хранит права, которые администратор включает в таблице BX.UI.AccessRights. Значения VALUE_NO = 0 и VALUE_YES = 1 уже определены в базовом PermissionDictionary.
<?php
namespace Bitrix\Example\Access\Permission;
class PermissionDictionary extends \Bitrix\Main\Access\Permission\PermissionDictionary
{
public const EXAMPLE_CREATE = 'example_create';
public const EXAMPLE_EDIT_OWN = 'example_edit_own';
public const EXAMPLE_EDIT_ALL = 'example_edit_all';
}
Добавьте языковой файл рядом с PermissionDictionary. Метод PermissionDictionary::getPermission() возьмет из него название права и подсказку для таблицы:
<?php $MESS['EXAMPLE_CREATE'] = 'Создание записей'; $MESS['EXAMPLE_EDIT_OWN'] = 'Редактирование своих записей'; $MESS['EXAMPLE_EDIT_ALL'] = 'Редактирование всех записей'; $MESS['HINT_EXAMPLE_EDIT_ALL'] = 'Право позволяет редактировать все записи модуля example.';
Создать таблицы ролей и разрешений
Для хранения прав нужны три ORM-класса:
ExampleRoleTableхранит роли с полямиIDиNAME;ExampleRoleRelationTableхранит привязки роли к пользователям, отделам и группам с полямиROLE_IDиRELATION;ExamplePermissionTableхранит значения прав роли с полямиROLE_ID,PERMISSION_IDиVALUE.
<?php
namespace Bitrix\Example\Access\Role;
use Bitrix\Main\Access\Role\AccessRoleTable;
class ExampleRoleTable extends AccessRoleTable
{
public static function getTableName(): string
{
return 'b_example_role';
}
}
<?php
namespace Bitrix\Example\Access\Role;
use Bitrix\Main\Access\Role\AccessRoleRelationTable;
class ExampleRoleRelationTable extends AccessRoleRelationTable
{
public static function getTableName(): string
{
return 'b_example_role_relation';
}
}
<?php
namespace Bitrix\Example\Access\Permission;
use Bitrix\Main\Access\Permission\AccessPermissionTable;
class ExamplePermissionTable extends AccessPermissionTable
{
public static function getTableName(): string
{
return 'b_example_permission';
}
}
Создайте эти таблицы при установке модуля. Базовые ORM-классы описывают поля, но не создают таблицы в базе данных автоматически.
Создайте утилитный класс для работы с ролями. Он связывает базовый RoleUtil с ORM-классами модуля:
<?php
namespace Bitrix\Example\Access\Role;
use Bitrix\Main\Access\Role\RoleUtil;
class ExampleRoleUtil extends RoleUtil
{
protected static function getRoleTableClass(): string
{
return ExampleRoleTable::class;
}
protected static function getRoleRelationTableClass(): string
{
return ExampleRoleRelationTable::class;
}
protected static function getPermissionTableClass(): string
{
return \Bitrix\Example\Access\Permission\ExamplePermissionTable::class;
}
protected static function getRoleDictionaryClass(): ?string
{
return null;
}
}
Создать модель объекта и контроллер
Модель объекта должна реализовать Bitrix\Main\Access\AccessibleItem. Если правило проверяет владельца записи, добавьте в модель метод, который вернет идентификатор владельца.
<?php
namespace Bitrix\Example\Access;
use Bitrix\Main\Access\AccessibleItem;
class ExampleModel implements AccessibleItem
{
private int $id;
private int $ownerId;
public function __construct(int $id, int $ownerId)
{
$this->id = $id;
$this->ownerId = $ownerId;
}
public static function createFromId(int $itemId): AccessibleItem
{
$row = ExampleTable::getRow([
'select' => ['ID', 'CREATED_BY'],
'filter' => ['=ID' => $itemId],
]);
if (!$row)
{
return new self(0, 0);
}
return new self((int)$row['ID'], (int)$row['CREATED_BY']);
}
public function getId(): int
{
return $this->id;
}
public function getOwnerId(): int
{
return $this->ownerId;
}
}
Контроллер загружает пользователя и объект. Для действий без объекта loadItem() может вернуть null, потому что BaseAccessController допускает ?AccessibleItem.
<?php
namespace Bitrix\Example\Access;
use Bitrix\Main\Access\AccessibleItem;
use Bitrix\Main\Access\BaseAccessController;
use Bitrix\Main\Access\Model\UserModel;
use Bitrix\Main\Access\User\AccessibleUser;
class ExampleAccessController extends BaseAccessController
{
protected function loadItem(?int $itemId = null): ?AccessibleItem
{
if (!$itemId)
{
return null;
}
return ExampleModel::createFromId($itemId);
}
protected function loadUser(int $userId): AccessibleUser
{
return UserModel::createFromId($userId);
}
}
Создать правила
Каждое действие должно иметь отдельное правило в пространстве имен Bitrix\Example\Access\Rule. Правило наследует AbstractRule и реализует execute(?AccessibleItem $item = null, $params = null): bool.
<?php
namespace Bitrix\Example\Access\Rule;
use Bitrix\Example\Access\Permission\PermissionDictionary;
use Bitrix\Main\Access\AccessibleItem;
use Bitrix\Main\Access\Rule\AbstractRule;
class CreateRule extends AbstractRule
{
public function execute(?AccessibleItem $item = null, $params = null): bool
{
if ($this->user->isAdmin())
{
return true;
}
return $this->user->getPermission(PermissionDictionary::EXAMPLE_CREATE)
=== PermissionDictionary::VALUE_YES;
}
}
<?php
namespace Bitrix\Example\Access\Rule;
use Bitrix\Example\Access\ExampleModel;
use Bitrix\Example\Access\Permission\PermissionDictionary;
use Bitrix\Main\Access\AccessibleItem;
use Bitrix\Main\Access\Rule\AbstractRule;
class EditRule extends AbstractRule
{
public function execute(?AccessibleItem $item = null, $params = null): bool
{
if ($this->user->isAdmin())
{
return true;
}
if (
$this->user->getPermission(PermissionDictionary::EXAMPLE_EDIT_ALL)
=== PermissionDictionary::VALUE_YES
)
{
return true;
}
if (!$item instanceof ExampleModel)
{
return false;
}
return $item->getOwnerId() === $this->user->getUserId()
&& $this->user->getPermission(PermissionDictionary::EXAMPLE_EDIT_OWN)
=== PermissionDictionary::VALUE_YES;
}
}
Подготовить данные для BX.UI.AccessRights
Компонент BX.UI.AccessRights принимает два массива:
accessRights— секции и права, которые нужно показать в таблице;userGroups— роли, участники ролей и текущие значения прав.
<?php
namespace Bitrix\Example\Access\Component;
use Bitrix\Example\Access\Permission\PermissionDictionary;
use Bitrix\Example\Access\Role\ExampleRoleUtil;
use Bitrix\Main\Access\AccessCode;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\UI\AccessRights\DataProvider;
class ConfigPermissions
{
public function getAccessRights(): array
{
return [
[
'sectionTitle' => Loc::getMessage('EXAMPLE_ACCESS_SECTION') ?? 'Example',
'rights' => [
PermissionDictionary::getPermission(PermissionDictionary::EXAMPLE_CREATE),
PermissionDictionary::getPermission(PermissionDictionary::EXAMPLE_EDIT_OWN),
PermissionDictionary::getPermission(PermissionDictionary::EXAMPLE_EDIT_ALL),
],
],
];
}
public function getUserGroups(): array
{
$roles = [];
$rows = ExampleRoleUtil::getRoles();
foreach ($rows as $row)
{
$roleId = (int)$row['ID'];
$roles[] = [
'id' => $roleId,
'title' => $row['NAME'],
'accessRights' => $this->getRoleAccessRights($roleId),
'members' => $this->getRoleMembers($roleId),
];
}
return $roles;
}
private function getRoleAccessRights(int $roleId): array
{
$rights = [];
$permissions = (new ExampleRoleUtil($roleId))->getPermissions();
foreach ($permissions as $permissionId => $value)
{
$rights[] = [
'id' => $permissionId,
'value' => (string)$value,
];
}
return $rights;
}
private function getRoleMembers(int $roleId): array
{
$members = [];
$rows = (new ExampleRoleUtil($roleId))->getMembers();
while ($row = $rows->fetch())
{
$accessCode = (string)$row['RELATION'];
$code = new AccessCode($accessCode);
$member = (new DataProvider())->getEntity($code->getEntityType(), $code->getEntityId());
$members[$accessCode] = $member->getMetaData();
}
return $members;
}
}
Для секции добавьте языковую строку:
<?php $MESS['EXAMPLE_ACCESS_SECTION'] = 'Права на записи';
Передать данные в компонент настройки прав
В PHP-компоненте подготовьте данные:
<?php
use Bitrix\Example\Access\Component\ConfigPermissions;
class ExampleConfigPermissionsComponent extends CBitrixComponent
{
public function executeComponent(): void
{
$configPermissions = new ConfigPermissions();
$this->arResult['USER_GROUPS'] = $configPermissions->getUserGroups();
$this->arResult['ACCESS_RIGHTS'] = $configPermissions->getAccessRights();
$this->includeComponentTemplate();
}
}
В шаблоне передайте подготовленные массивы в BX.UI.AccessRights. Подробное описание параметров, подключения bitrix:main.ui.selector и панели кнопок смотрите в статье о компоненте BX.UI.AccessRights.
<?php
use Bitrix\Main\UI\Extension;
Extension::load(['ui.buttons', 'ui.icons', 'ui.notification', 'ui.accessrights']);
?>
<div id="example-config-permissions"></div>
<script>
let AccessRights = new BX.UI.AccessRights({
component: 'bitrix:example.config.permissions',
renderTo: document.getElementById('example-config-permissions'),
userGroups: <?= CUtil::PhpToJSObject($arResult['USER_GROUPS']) ?>,
accessRights: <?= CUtil::PhpToJSObject($arResult['ACCESS_RIGHTS']) ?>,
});
AccessRights.draw();
</script>
Сохранить роли и права
При сохранении BX.UI.AccessRights вызывает action save у указанного компонента. В запросе приходит массив userGroups. Для каждой роли компонент передает:
id— идентификатор роли, для новой роли приходит0;title— название роли;accessRights— массив прав с полямиidиvalue;accessCodes— привязки роли к пользователям, отделам и группам.
<?php
use Bitrix\Example\Access\Role\ExampleRoleUtil;
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Engine\Controller;
class ExampleConfigPermissionsAjaxController extends Controller
{
public function configureActions(): array
{
return [
'save' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod(['POST']),
new ActionFilter\Csrf(),
],
],
];
}
public function saveAction(array $userGroups = []): void
{
foreach ($userGroups as $roleSettings)
{
$this->saveRoleSettings($roleSettings);
}
}
private function saveRoleSettings(array $roleSettings): void
{
$roleId = (int)$roleSettings['id'];
$roleTitle = (string)$roleSettings['title'];
if ($roleId === 0)
{
$roleId = ExampleRoleUtil::createRole($roleTitle);
}
if ($roleId <= 0)
{
return;
}
$role = new ExampleRoleUtil($roleId);
$role->updateTitle($roleTitle);
$permissions = array_combine(
array_column($roleSettings['accessRights'] ?? [], 'id'),
array_column($roleSettings['accessRights'] ?? [], 'value')
);
$role->updatePermissions($permissions ?: []);
$role->updateRoleRelations($roleSettings['accessCodes'] ?? []);
}
}
Проверить доступ в коде модуля
Проверяйте доступ перед действием пользователя. Для создания запись еще не существует, поэтому идентификатор объекта можно не передавать:
<?php
use Bitrix\Example\Access\ActionDictionary;
use Bitrix\Example\Access\ExampleAccessController;
if (!ExampleAccessController::can($userId, ActionDictionary::ACTION_CREATE))
{
ShowError('Access denied');
return;
}
Для редактирования передайте идентификатор записи. Контроллер загрузит объект через loadItem() и передаст его в EditRule::execute():
<?php
use Bitrix\Example\Access\ActionDictionary;
use Bitrix\Example\Access\ExampleAccessController;
if (!ExampleAccessController::can($userId, ActionDictionary::ACTION_EDIT, $exampleId))
{
ShowError('Access denied');
return;
}
Особенности
- Правило должно называться по коду действия. Для
ACTION_EDIT = 'edit'создайте классEditRule, а неExampleEditRule. - Метод
BaseAccessController::getRuleName()помечен как устаревший. Используйте стандартный поиск правил черезRuleControllerFactory. - Значение права для
togglerхранится как0или1. В правилах сравнивайте результатgetPermission()сPermissionDictionary::VALUE_YES. - Для сохранения ролей используйте наследник
RoleUtil: он проверяет access code и сохраняет права в формате, который ожидают базовые таблицы доступа. - Таблица
BX.UI.AccessRightsпоказывает только те права, которые переданы вaccessRights, и только те роли, которые переданы вuserGroups. - Компонент настройки прав не заменяет проверку доступа в бизнес-логике. Проверку нужно вызывать в компонентах, контроллерах и обработчиках, которые выполняют действия пользователя.