Права доступа в таблицах React: один слой политики для всех действий
Права доступа в таблицах React часто ломаются, когда правила живут внутри рендереров ячеек. Используйте один слой политики, чтобы действия в строках, массовые действия и экспорт всегда совпадали.

Почему таблицы расходятся с правилами
Таблица часто начинается с одной маленькой проверки прав внутри рендерера ячейки. Если пользователь может редактировать, показываем кнопку. Если нет — прячем её. Для экрана с несколькими колонками и одной-двумя действиями это кажется нормальным.
Потом страница растёт. Кто-то добавляет панель массовых действий. Кто-то другой — меню строки, кнопку экспорта, панель с деталями и быстрый вызов в другой части экрана. У каждого элемента появляется своё маленькое правило, написанное в разное время разными людьми.
Вот тогда и начинается расхождение. В строке может скрываться «Удалить», а в массовом меню тот же пользователь всё ещё может удалить эти записи. Панель с деталями может блокировать «Одобрить», но быстрый вызов над таблицей всё равно это разрешает. Экспорт может включать поля, которые видимая таблица пыталась защитить. На первый взгляд экран выглядит единым, но правила не совпадают.
Права доступа в таблицах React обычно ломаются по простой причине: рендереры ячеек знают только про одну кнопку в одном месте. Они не знают всей политики страницы. Они отвечают на вопрос «показывать ли здесь этот элемент?», хотя настоящий вопрос шире: «может ли этот пользователь выполнить это действие с этими записями где угодно на этом экране?»
Проблема становится хуже, когда правил становится больше. Проверка роли — только начало. Потом правило зависит ещё и от статуса записи, владельца, команды, уровня тарифа, feature flags или того, заблокирован ли элемент другим процессом. Один разработчик проверяет роль и статус. Другой — роль и владельца. Третий копирует первую проверку, но забывает про статус. Никто ничего не замечает, пока пользователь не попадёт не туда.
Пользователь очень быстро чувствует несоответствие:
- Кнопка исчезает в одной строке, но то же действие работает в другом месте.
- Массовые действия позволяют выбрать записи, которые на самом деле нельзя менять.
- Экспорт включает данные, которые в других частях экрана считались закрытыми.
- Сервер отклоняет действие после того, как интерфейс уже как будто разрешил его.
Именно последний вариант наносит больше всего вреда. Люди перестают доверять таблице. Они уже не понимают, означает ли отключённая кнопка «никогда», «не для этой строки» или «попробуйте в другом месте». Когда экран подаёт смешанные сигналы, пользователи считают продукт ненадёжным, даже если правило на сервере правильное.
Таблица не должна придумывать правила доступа на лету при рендеринге каждой ячейки. Она должна читать одно общее решение и применять его везде, где появляется одно и то же действие.
Что должен решать один слой политики
Хороший слой политики отвечает на одни и те же вопросы для каждого действия на экране ещё до того, как какая-то ячейка попытается показать кнопку. Так права доступа в таблицах React остаются согласованными для меню строк, массовых действий, кнопок экспорта, горячих клавиш и любого другого триггера, который вы добавите позже.
Ячейка таблицы не должна знать, почему пользователь может архивировать одну запись, редактировать другую или экспортировать только часть списка. Код ячейки должен просто получить ответ и отрисовать его. Сама логика правила должна жить в одном месте.
Что отвечает политика
Для каждого действия слой политики должен возвращать небольшой и понятный результат, который может использовать любой участок интерфейса:
- нужно ли вообще показывать действие
- разрешено ли действие прямо сейчас
- если действие запрещено, какое короткое объяснение показать пользователю
- относится ли действие к этой строке, к выбранным строкам или ко всему отфильтрованному результату
- можно ли выбрать строку для массовых действий
Эти ответы похожи, но это не одно и то же. Видимость — это вопрос о том, должен ли пользователь вообще видеть действие. Разрешение — может ли он использовать его прямо сейчас. Отключённое состояние — это середина: действие видно, но приложение блокирует его из-за состояния строки, ограничений тарифа, статуса согласования или другого бизнес-правила.
Такое разделение важно. Финансовый администратор может видеть «Экспорт» на каждом экране отчётов, но кнопка может оставаться отключённой, пока он не выберет хотя бы одну одобренную запись. Сотрудник поддержки может вообще никогда не видеть «Удалить». Если оба правила живут внутри случайных ячеек, страница быстро начинает расходиться.
Держите бизнес-логику вне кода таблицы и возвращайте простые булевы значения или небольшие объекты. Например, функция политики может отвечать на вопросы вроде canEdit, showDelete, disableArchiveReason, canSelectRow и canExportSelection. Такой формат легко переиспользовать в панели инструментов, контекстном меню, нижней сводке или окне экспорта.
Простые результаты лучше сложных абстракций. Большинству команд проще работать с результатом политики, который читается как обычный текст, чем с глубокой системой прав, которую понимает только один человек.
Если вам нужен один тест для этой части экрана, используйте такой вопрос: могут ли ячейки таблицы, панель массовых действий и поток экспорта обращаться к одной и той же функции политики и получать одинаковый ответ для одного и того же набора строк? Если нет, правила всё ещё лежат не там, где нужно.
Простой пример, который показывает проблему
Одна из типичных ошибок с правами доступа в таблицах React начинается с одной безобидной проверки внутри рендерера ячейки. Строка выглядит правильно, и экран кажется корректным. Потом пользователь нажимает массовое действие или экспортирует данные, и правила меняются у него прямо под рукой.
Представьте таблицу сотрудников с тремя ролями:
- Администратор может редактировать, деактивировать и экспортировать всех сотрудников.
- Менеджер может работать только с людьми из своей команды.
- Наблюдатель может только читать ограниченные данные и не может ничего менять.
Теперь допустим, менеджер открывает таблицу. В каждой строке в колонке действий есть кнопка «Деактивировать». Внутри рендерера этой ячейки код проверяет, относится ли запись к команде менеджера. Если нет — кнопка исчезает. На экране всё выглядит нормально. Менеджер видит кнопку для сотрудников Sales и не видит её для Support или Finance.
Проблема начинается тогда, когда у таблицы ещё есть чекбоксы и массовое действие «Деактивировать выбранные» в панели инструментов. Эта панель часто использует другое правило. Иногда она проверяет только роль текущего пользователя и забывает о каждой выбранной строке. Менеджер может отметить десять строк, включая людей не из Sales, нажать массовое действие, и приложение отправит на сервер все десять идентификаторов. В кнопке строки был ответ «нет», а в массовом действии — «да».
Экспорт ломается похожим образом. Таблица может скрывать строки, которые менеджер видеть не должен, или скрывать поля зарплаты и личные данные для наблюдателей. Если код экспорта строит CSV из сырых данных вместо тех же правил доступа, файл включает строки и колонки, которые экран уже спрятал. Страница выглядела безопасной. Загруженный файл — уже нет.
Вот почему права доступа в таблицах React так легко расходятся. Рендерер ячейки принимает одно решение, панель инструментов — другое, а код экспорта — третье. Пользователи быстро замечают несоответствие, потому что таблицы держат на одном экране и действия в строке, и массовые действия, и фильтры, и выбор, и экспорт.
Когда один и тот же экран может говорить о той же записи три разных вещи, баги перестают выглядеть как баги. Они выглядят случайными. Обычно именно в этот момент команда понимает, что правила не должны жить внутри ячеек.
Как перенести правила шаг за шагом
Сначала соберите в одном месте все входные данные, которые влияют на права доступа. Используйте текущего пользователя, его роль и тот статус строки, который действительно важен: ownerId, status, lockedAt или archived. Если ячейки сами подтягивают куски этих данных, две части одной таблицы начнут спорить друг с другом.
Затем напишите по одной небольшой функции политики для каждого действия. Держите каждую функцию узкой: canEditInvoice, canDeleteInvoice, canExportInvoice. Каждая должна отвечать на один конкретный вопрос и возвращать данные, а не JSX и не состояние компонента.
Понятная форма результата сильно упрощает тестирование. Возвращайте что-то вроде { allowed: true } или { allowed: false, reason: "paid invoice" }. Вы сможете подставлять в тестах фиктивных пользователей и фиктивные строки и сравнивать возвращаемый объект без рендера таблицы.
Обычно чистый переход выглядит так:
- Соберите один входной объект из данных вошедшего пользователя и строки.
- Запускайте функции политики при подготовке строк для таблицы.
- Сохраняйте результаты рядом с каждой строкой, например
rowPolicy.deleteиrowPolicy.export. - Читайте те же результаты в ячейках, в панелях массовых действий и в коде экспорта.
- Включите отладочное логирование для странных случаев, чтобы видеть, почему правило заблокировало действие.
Общий результат важнее, чем названия функций. Если действие в строке проверяет одно правило, а панель массовых действий — другое, у вас всё тот же баг, только в другом месте. Ячейки не должны самостоятельно решать вопросы прав, и путь экспорта не должен позже изобретать свою версию.
Небольшой пример хорошо показывает пользу. Допустим, менеджер может архивировать черновики заказов, но не может архивировать уже отправленные. Если рендерер ячейки прячет кнопку Archive для отправленных строк, а массовое действие проверяет только роль пользователя, пользователь всё равно сможет выбрать отправленные строки и архивировать их массово. Когда обе части читают один и тот же результат политики, меню строки и панель массовых действий всегда совпадают.
Логирование помогает, когда случай выглядит случайным. Если кто-то говорит: «Вчера я мог это экспортировать», — залогируйте входные данные и решение: роль, владение, статус записи, имя действия, allowed и reason. Такой лог обычно за минуты указывает на расхождение.
Права доступа в таблицах React становятся намного менее хрупкими, когда каждое действие читает один и тот же результат политики.
Как привязать весь экран к одним и тем же правилам
Таблицы начинают путаться, когда каждая часть экрана принимает собственное решение о правах. Меню строки проверяет одно, панель массовых действий — другое, а экспорт вообще пропускает правила. Пользователь это замечает быстро. Он видит действие в одном месте, а потом получает блокировку в другом.
Лучшее решение простое: для каждой строки запускайте одну политику, сохраняйте результат вместе с этой строкой и пусть все действия читают его оттуда. Права доступа в таблицах React становятся надёжнее, когда экран перестаёт гадать.
Для каждой строки политика должна возвращать понятные ответы вроде canEdit, canDelete, canExport и ещё одну короткую причину, если действие заблокировано. Этот результат должен идти вместе с данными строки, а не жить внутри рендерера ячейки. Тогда меню строки сможет показывать только допустимые действия или отключать одно действие с точной причиной, которую использует остальная часть экрана.
То же правило должно управлять такими местами:
- меню действий строки
- панель массовых действий
- поток экспорта
- окно подтверждения или сообщение об ошибке
С массовыми действиями нужно быть особенно аккуратными. Пользователь часто выбирает 20 строк ещё до того, как нажмёт хоть что-то. Если приложение узнает только в момент API-запроса, что 6 строк не подходят, экран ощущается сломанным. Проверьте выбранные строки заранее тем же результатом политики, который у вас уже есть. Затем либо заблокируйте массовое действие сразу, либо продолжайте только с допустимыми строками и точно объясните, что произошло.
Короткое сообщение работает лучше, чем общий текст ошибки. «6 строк были пропущены, потому что архивные заказы нельзя отменить» — этого достаточно. Люди могут поправить выбор и двигаться дальше.
Экспорт должен работать так же. Не позволяйте кнопке экспорта брать сырые данные таблицы и решать всё потом. Собирайте набор для экспорта только из строк, которые уже прошли canExport. Если какие-то строки не прошли, скажите пользователю, сколько из них было исключено и почему. Это особенно важно, когда в таблице смешаны записи с разным статусом, владельцем или типом аккаунта.
Одно строковое объяснение для каждого заблокированного действия делает экран последовательным. Используйте одинаковую формулировку в отключённом пункте меню, подсказке, предупреждении о массовом действии и сообщении об экспорте. Если ваша политика возвращает не готовый текст, а код причины, сопоставьте каждому коду одно предложение в слое интерфейса. Так правила останутся чистыми, а язык — дружелюбным.
Когда правило меняется, вы обновляете одну политику, и вместе с ней меняется вся таблица. Меню строк, массовые действия и экспорт перестают спорить друг с другом.
Ошибки, из-за которых баги трудно найти
Многие ошибки с правами в таблицах сначала вовсе не выглядят как ошибки прав. В одной строке действие пропадает, а экспорт всё равно включает закрытые поля. Массовое действие остаётся доступным для записей, к которым у пользователя не должно быть доступа. Экран кажется случайным, потому что каждый его кусок принимает собственное решение.
Одна из частых ошибок — спрятать кнопку в интерфейсе и назвать это решением. Если сервер не проверяет то же правило, кто угодно всё равно сможет вызвать действие через сохранённый запрос, скрипт или устаревшее состояние клиента. Кнопка — это только подсказка для пользователя. Сервер всё равно должен принимать решение.
Ещё одна проблема появляется, когда команды копируют одно и то же правило в каждый файл колонки. Сначала это кажется быстрым решением на неделю, а потом в одном файле проверяется canEdit, в другом — mayEdit, а в третьем имя роли используется напрямую. Небольшие различия накапливаются. Через несколько месяцев одна строка таблицы ведёт себя иначе, чем массовое меню, и никто не понимает почему.
Правила становятся грязными, когда в них просачивается состояние интерфейса
Права должны отвечать только на один вопрос: «Может ли этот пользователь выполнить это действие с этой записью?» Они не должны ещё и зависеть от случайных деталей экрана — например, раскрыта ли строка, выбран ли чекбокс или открыта ли модалка.
Роль пользователя и индикатор загрузки — это не один и тот же тип входных данных. Когда код смешивает их, баги прячутся в пограничных случаях. Например, выбранная строка может разблокировать массовое действие, хотя одна из выбранных записей должна оставаться заблокированной.
Экспорт часто вызывает самые неприятные сюрпризы. Команды тратят время на проверку видимых кнопок, а потом позволяют коду экспорта читать сырые данные ещё до того, как сработают фильтры политики. Это создаёт тихую утечку. Таблица выглядит безопасной, а CSV рассказывает совсем другую историю.
Здесь помогает простое правило:
- Используйте один слой политики для решения, какие действия разрешены
- Используйте тот же слой политики для действий в строках, массовых действий и экспорта
- Держите серверные проверки отдельно от состояния интерфейса
- Используйте одинаковые названия политик во всём приложении
Имена важнее, чем многие ожидают. Если одна часть экрана запрашивает delete_order, а другая — remove_order, у вас уже две разные правила, даже если вы этого не планировали. Такие баги проходят ревью, потому что оба названия звучат разумно.
Такое часто происходит в быстро работающих продуктовых командах. Кто-то добавляет быстрый вызов в рендерере ячейки, кто-то позже добавляет логику экспорта, и оба выпускают разные проверки. Олег Сотников часто подталкивает команды к одному общему слою политики именно по этой причине: код становится проще доверять, а странные разрывы в правах перестают попадать в продакшен.
Короткая проверка перед релизом
Таблица может выглядеть правильно в короткой демо-сцене и всё равно ломаться в тот момент, когда реальный пользователь начинает кликать. Самый безопасный последний шаг — выбрать несколько строк и вручную пройти все пути действия: меню строки, панель массовых действий, горячую клавишу, модальное подтверждение и экспорт. Если один путь говорит «разрешено», а другой — «заблокировано», значит правила всё ещё живут в нескольких местах.
Права доступа в таблицах React обычно ломаются на границах, а не на очевидном пути. Проверка одной строки важна, но смешанные выборки важнее. Именно там быстрее всего всплывают скрытые различия правил.
Возьмите небольшой набор для теста, в котором есть обычные строки, заблокированные строки, архивные строки и хотя бы одна строка, которую пользователь должен только просматривать. Затем проверьте следующее:
- Пройдите одну строку через каждый способ запуска действия. Если «Редактировать» скрыто в меню ячейки, но всё ещё работает из панели массовых действий или по горячей клавише, экран рассинхронизирован.
- Выберите смешанный набор строк. Например, три редактируемые записи и одну закрытую. Таблица должна либо заблокировать действие с понятной причиной, либо обработать только разрешённые строки так, чтобы пользователь это понял.
- Проверьте пустые и почти пустые состояния. Отключённая кнопка должна объяснять, почему она отключена. Пустой экспорт, серое действие или отсутствующий пункт меню не должны казаться случайными.
- Сравните экспорт с правилами действий. Если пользователи не могут открыть или изменить определённые строки, экспорт не должен тихо включать эти строки или дополнительные поля, к которым у них нет доступа на экране.
- Прочитайте все сообщения об ошибках вслух. «Действие не выполнено» не говорит пользователю ничего. «Архивировать можно только свои строки» — гораздо лучше, и команде это тоже помогает быстрее увидеть расхождение правил.
Один небольшой сценарий ловит очень много багов: выберите пять строк, где две разрешены, две заблокированы и одна уже находится в целевом состоянии. Затем протестируйте массовое обновление, массовое удаление и экспорт. Такая смесь часто выявляет ошибки в счёте, неверные состояния отключения и запутанный текст подтверждения.
Следите и за цифрами в интерфейсе. Если на панели написано «выбрано 5», а в окне подтверждения — «изменится 3 элемента», экран должен объяснить почему. Тихая фильтрация заставляет людей думать, что приложение потеряло данные.
Хорошая финальная проверка проста: тот же пользователь, те же строки, тот же результат, где бы он ни нажал. Если это работает, ваш слой политики, скорее всего, делает своё дело.
Что делать дальше
Сначала возьмите ту таблицу, которая создаёт больше всего шума. Ищите ту, из-за которой чаще всего приходят вопросы в поддержку, странные сообщения от пользователей или повторяющиеся споры внутри команды. Обычно именно в такой таблице правила доступа разбросаны по кнопкам в ячейках, меню строк, массовым действиям и экспорту.
Начинайте с малого. Не нужно перестраивать весь экран за один проход. Переносите по одному действию в общий код политики, а потом подключайте к тому же решению все части интерфейса.
Рабочий порядок обычно такой:
- Перечислите все действия в этой таблице, включая действия в строках, массовые действия, экспорт и скрытые быстрые вызовы.
- Напишите одну функцию политики для одного действия с одинаковыми входными данными каждый раз.
- Сначала замените проверку в рендерере ячейки, а потом переиспользуйте её в панели инструментов, меню и пути экспорта.
- Удалите старые встроенные проверки сразу после того, как общая правило заработает.
Вот тут многие команды ошибаются. Они чинят видимую кнопку, но оставляют логику массового действия без изменений. Пользователь всё ещё может выбрать 50 строк и запустить то, что меню строки бы заблокировало. У кода экспорта часто та же проблема, потому что он живёт далеко от интерфейса таблицы.
Сначала проверьте именно эти два пути. Если у вас есть время только на несколько тестов, начните с массовых действий и экспорта. Они затрагивают больше записей, дают более серьёзные ошибки и часто обходят те проверки, которые человек видит на экране.
Держите тесты простыми. Возьмите несколько реалистичных ролей, несколько состояний записей и несколько действий, которыми люди действительно пользуются. Например, проверьте, может ли менеджер редактировать одну строку, редактировать много строк и экспортировать тот же отфильтрованный набор. Если ответы отличаются, ваша политика всё ещё живёт в нескольких местах.
Права доступа в таблицах React становится намного легче понимать, когда одно правило решает каждое действие на экране. Код тоже упрощается. Новые действия перестают превращаться в копипасту проверок прав.
Если ваша команда продолжает находить крайние случаи или не может договориться, где должны жить правила, внешний взгляд может сэкономить время. Fractional CTO вроде Олега может посмотреть на поток прав доступа, продуктовые правила и архитектуру экрана, а потом предложить более чистую схему, не переворачивая приложение с ног на голову. Такой обзор особенно полезен, когда таблица уже влияет на биллинг, операции или данные клиентов.