AccessControl в Solidity: роли редакции
Владелец удобно управляет небольшим проектом, но редакции может понадобиться разделение обязанностей. Автор объявляет новый выпуск, редактор отзывает ошибочный материал, администратор назначает участников. Для такого требования создадим отдельный EditorialCatalog с ролями AccessControl.
Это не добавление людей к immutable publisher первоначального реестра. Новый контракт имеет другой конструктор и другие проверки. Публикация и деактивация сохраняют форму данных курса, а право каждой операции определяется своей ролью. Библиотека зафиксирована на OpenZeppelin Contracts 5.4.0.
Идентификатор роли
Роль обозначается bytes32. Для именованных ролей удобно использовать константу от известной строки. Такой хеш является идентификатором, а не доказательством безопасности и не отпечатком манифеста. Имя должно быть стабильным, чтобы клиент мог сопоставить право с операцией.
В нашей модели PUBLISHER_ROLE разрешает publish, REVOKER_ROLE разрешает deactivate. DEFAULT_ADMIN_ROLE по начальному правилу управляет назначением ролей. Само наличие административной роли не даёт доступ к методу onlyRole(PUBLISHER_ROLE), если администратору эта роль отдельно не назначена.
Полный редакционный реестр
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract EditorialCatalog is AccessControl {
error InvalidAccount();
error InvalidCourse();
error NotActive();
bytes32 public constant PUBLISHER_ROLE = keccak256("PUBLISHER_ROLE");
bytes32 public constant REVOKER_ROLE = keccak256("REVOKER_ROLE");
struct Course { bytes32 contentHash; bool active; }
mapping(uint256 => Course) public courses;
event Published(uint256 indexed courseId, bytes32 contentHash);
event Deactivated(uint256 indexed courseId);
constructor(address admin, address publisher_, address revoker) {
if (admin == address(0) || publisher_ == address(0)
|| revoker == address(0)) revert InvalidAccount();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(PUBLISHER_ROLE, publisher_);
_grantRole(REVOKER_ROLE, revoker);
}
function publish(uint256 id, bytes32 digest)
external onlyRole(PUBLISHER_ROLE)
{
if (id == 0 || digest == bytes32(0)) revert InvalidCourse();
courses[id] = Course(digest, true);
emit Published(id, digest);
}
function deactivate(uint256 id) external onlyRole(REVOKER_ROLE) {
if (!courses[id].active) revert NotActive();
courses[id].active = false;
emit Deactivated(id);
}
}
В конструкторе первоначальные адреса проверяются на ноль. _grantRole является внутренним механизмом назначения, поэтому там выполняется начальная настройка. Позднее внешние grantRole и revokeRole имеют свои административные проверки. Нельзя вывести _grantRole в общедоступный помощник без отдельного условия.
Разделённые действия
Пусть P имеет только издательскую роль, V — только роль отзыва, A — только административную. P публикует H1, но не отключает. V отключает активный 17, но не назначает H2. A может назначить P или V по текущему графу, но не проходит их операционные модификаторы автоматически.
Последнее ограничение не означает, что A бессилен относительно публикации. Он может выдать себе нужную роль при соответствующем административном праве. Поэтому анализ полномочий должен учитывать прямые действия и возможность назначать их. Разделение ролей полезно, но не создаёт независимость от всемогущего администратора, если он управляет всеми назначениями.
Публикация после отзыва снова активирует курс. В нашей модели P способен вернуть активность через новый publish. Следовательно, роль REVOKER не имеет окончательного вето над всеми будущими выпусками. Если редакции требуется блокировка, которую издатель не снимает, понадобится дополнительный статус и правило. Названия ролей не вводят такое ограничение автоматически.
Проверка текущего права
Клиент читает hasRole(role,address) для выбранного экземпляра. Результат относится к текущему состоянию. Удаление аккаунта из серверного списка сотрудников не отзывает роль на цепочке, пока соответствующее действие не исполнено. И наоборот, отозванная роль не возвращается от входа в старую сессию сайта.
| Субъект | Publish | Deactivate | Grant обычной роли |
|---|---|---|---|
| Только PUBLISHER | Да | Нет | Нет |
| Только REVOKER | Нет | Да | Нет |
| Только DEFAULT_ADMIN | Не напрямую | Не напрямую | Да по начальному графу |
| Без ролей | Нет | Нет | Нет |
Один адрес может иметь несколько ролей. Это допустимое состояние AccessControl, а не ошибка библиотеки. Если организация требует разные адреса, нужно выразить такое правило назначений или соблюдать подтверждённую процедуру. Простое определение двух констант не запрещает объединение обязанностей.
Перечень участников
Обычный AccessControl не предоставляет автоматический on-chain-список всех участников роли. Их можно обнаруживать по событиям и внешнему индексу либо использовать отдельное расширение с перечислением. Для короткой панели управления это нужно учитывать заранее: hasRole отвечает на вопрос об известном адресе, а не выдаёт всю редакцию.
Индекс ролей может отставать, как индекс публикаций. Перед важным действием проверяется актуальное право и его администратор. При недоступном RPC не следует показывать «роль отозвана»: результат неизвестен. Такая точность остаётся частью удобного интерфейса.
Администратор тоже является риском
Начальный DEFAULT_ADMIN_ROLE управляет обычными ролями и сам собой. Если этот субъект утрачен или скомпрометирован, набор прав может измениться сильно. Для главного администратора библиотека предлагает отдельный механизм задержанного перехода, который рассмотрим позже.
Пока результатом стало ясное разделение операций и их назначений. Следующая страница нарисует граф администраторов, чтобы понять, кто способен выдавать каждое право и почему название «редактор» недостаточно. Исходник AccessControl 5.4.0 описывает выбранные правила внешних и внутренних методов.
Две роли и право повторной активации
У нашей публикации сохранена прежняя семантика: полный присвоенный Course содержит active=true. Следовательно, издатель P способен вернуть курс после отзыва V, опубликовав H2. Если задача редакции предполагает, что V блокирует курс до отдельного разрешения, показанный код её не решает. Нужно добавить состояние блокировки и новый переход разблокирования.
Это полезный пример анализа ролей через состояние. По таблице методов кажется, что обязанности разделены полностью, но комбинация действий создаёт косвенное влияние. При проектировании стоит читать последовательности, а не только одну функцию по очереди.
Есть и другой вариант: V отозвал H1 после того, как P уже успел опубликовать H2. Обычная deactivate отключит текущий H2, поскольку не принимает ожидаемый hash. Если отзыв должен относиться к конкретному выпуску, потребуется проверка ожидания или отдельный журнал статусов версий. Название revoker не выбирает такой договор автоматически.
Для самостоятельного разбора составьте последовательность publish H1, подготовка отзыва H1, publish H2, deactivate. Назовите фактический конечный hash и active. Отпечаток H2 сохраняется, флаг становится false. Это соответствует коду, хотя первоначальное человеческое намерение могло относиться к другому файлу.
В редакционной панели поэтому полезно показать свежие сведения непосредственно перед действием. Однако предупреждение не заменяет on-chain-условие, если требование должно соблюдаться при конкурирующих транзакциях. Права определяют субъекта, сравнение ожидаемого состояния определяет точность намерения. Разберите также адрес с обеими операционными ролями: он может и отозвать, и снова объявить выпуск. Если организация требует разделения людей, совместное назначение этому субъекту нужно исключить отдельной подтверждённой политикой, а не ожидать запрета от AccessControl.
Для самостоятельного уточнения: AccessControl.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.