Skip to content

ТЗ — API-токены (Sanctum) (cms/api-tokens)

Слой: 🔵 инфра-модуль · Зрелость доноров: ★★ · Донор: freelance Статус: ТЗ к разработке

Назначение и возможности

Административный слой поверх Sanctum: управление персональными токенами доступа из Filament без командной строки. Даёт видимость abilities, срока действия и активности токена, а также журнал его использования.

  • Выпуск токена с явным набором abilities и сроком действия;
  • список токенов пользователя с датой последнего использования;
  • принудительный отзыв токена (мгновенная инвалидация);
  • журнал использования токена (эндпоинт, IP, дата) для аудита;
  • rate-limit per-токен поверх общего лимита API;
  • уведомление владельцу при использовании токена с нового IP (интеграция с cms/notifications-bus).

Зависимости и выключение

requires: — · suggests: cms/notifications-bus · provides: api-tokens

При выключении управление токенами из Filament недоступно; ранее выпущенные токены продолжают работать через штатный Sanctum, но без rate-limit per-токен и журнала.

Модель данных

ТаблицаКлючевые поляПримечание
cms_api_token_metatoken_id (FK на personal_access_tokens), label, expires_at, rate_limit, last_ipРасширение стандартной таблицы Sanctum
cms_api_token_usage_logtoken_id, route, ip, used_atЖурнал вызовов (ротация по retention)

token_idconstrained('personal_access_tokens')->index(), expires_at — cast datetime, last_ip — nullable string, денормализация последнего IP (обновляется вместе с last_used_at тем же обработчиком, что пишет cms_api_token_usage_log — источник истины по истории IP; last_ip — только для списка без похода в журнал). cms_api_token_usage_log — журнальная append-only таблица, индекс/BRIN по used_at под retention и выборку в UI.

Whitelist abilities: допустимые значения при выпуске — не произвольные строки, а permissions, зарегистрированные манифестами включённых модулей (extra.cms.permissions, §2 стандарта); whitelist строится динамически из реестра ядра на момент валидации (Filament и FormRequest читают один реестр, не дублируют список), не хардкодится. Permission выключенного модуля из реестра выпадает — попытка указать его отклоняется как неизвестная строка; уже выпущенный токен с такой ability описан в «Крайние случаи» (403 на проверке, не 500).

Входные и выходные данные

Входы — всё, что не перечислено ниже, отвергается (422 либо игнорируется, whitelist-принцип §11 стандарта):

ИсточникДанные/поляЧем валидируется
Filament-форма выпуска токенаlabel, abilities[], ttl_days (или expires_at), rate_limit (опц.)Валидация Filament: whitelist abilities из зарегистрированных permissions, ttl_daysmax_ttl_days, abilities не пустой массив
POST /api/v1/admin/api-tokenslabel, abilities[], ttl_daysFormRequest: whitelist abilities по правам вызывающего, ttl_days в допустимом диапазоне, заголовок Idempotency-Key
DELETE /api/v1/admin/api-tokens/{id}id токенаRoute model binding + Policy (api-tokens.manage либо api-tokens.manage.own на свой токен)
GET /api/v1/admin/api-tokens/{id}/usageid токена, cursorRoute model binding + whitelist сортировки (только used_at)
Sanctum-middleware (внутренний перехват)route, ip, token_id текущего запросаНе пользовательский ввод — служебные поля запроса ядра, тело запроса не читается
Импорт / вебхукНе применимо: модуль не принимает внешние импорт-файлы и вебхуки

Выходы

ПотребительДанныеФормат
Filament / API-клиент (ответ на выпуск)plaintext-токен (единственный раз), id, label, abilities[], expires_atJSON {data, meta}; plaintext только в теле этого ответа, не логируется и не хранится
Filament / API (список токенов)токены без plaintext, last_used_at, last_ipkeyset-пагинация {data, meta.next_cursor}
Filament / API (журнал использования)route, ip, used_atkeyset-пагинация {data, meta.next_cursor}
cms/notifications-bus (через NotificationDispatch)user_id, token_id, ipPayload provides-контракта; при выключенном модуле — log-fallback
Подписчики события (например cms/audit, если включён)token_id, user_id, abilities[]Payload события ApiTokenIssued / ApiTokenRevoked

Настройки (группа api-tokens)

КлючТипДефолтaffectsPageCacheОписание
api-tokens.default_ttl_daysint90Срок действия токена по умолчанию
api-tokens.default_rate_limitint60Запросов в минуту на токен по умолчанию
api-tokens.usage_log_retention_daysint30Хранение журнала использования
api-tokens.notify_new_ipbooltrueУведомлять владельца при входе с нового IP
api-tokens.max_ttl_daysint365Верхняя граница ttl_days при выпуске — блокирует фактически бессрочные токены
api-tokens.max_active_per_userint10Лимит активных токенов на пользователя — выпуск сверх лимита отклоняется понятной ошибкой
api-tokens.rotation_grace_hoursint0Grace-период при ротации: 0 — старый токен отзывается мгновенно, >0 — работает до истечения окна
api-tokens.issuance_enabledbooltrueKill-switch: false отключает выпуск новых токенов (существующие продолжают работать), без выключения модуля целиком
api-tokens.auto_revoke_inactive_daysint180Токен без использования дольше этого срока отзывается автоматически джобой RevokeInactiveTokens; 0 — авто-отзыв выключен

API

МетодПутьДоступНазначение
GET/api/v1/admin/api-tokensapi-tokens.manageСписок токенов (keyset-пагинация; свои или всех — по правам)
POST/api/v1/admin/api-tokensapi-tokens.manageВыпуск токена (abilities, ttl)
DELETE/api/v1/admin/api-tokens/{id}api-tokens.manageОтзыв токена
GET/api/v1/admin/api-tokens/{id}/usageapi-tokens.manageЖурнал использования конкретного токена (keyset-пагинация)

POST /api/v1/admin/api-tokens обязан принимать заголовок Idempotency-Key — выпуск токена создаёт кред (мутация с внешним эффектом, §7 стандарта); ревизия ядра 14.07.2026, п.8 явно распространяет идемпотентность на выпуск токенов/ключей — повтор с тем же ключом отдаёт исходный ответ, не создаёт второй токен.

Компоненты

Filament: ресурс токенов (выпуск/отзыв, abilities-чекбоксы, дата последнего использования, last_ip), виджет «токены на истечении». Команды: cms:api-tokens:prune-expired --json (очистка просроченных), cms:api-tokens:doctor --json.

Demo-сидер: ApiTokenDemoSeeder создаёт один тестовый токен с ограниченными abilities (например pages.view) для playground/_gallery — виджет «токены на истечении» и ресурс списка показываются без ручного выпуска токена; plaintext demo-токена не хранится за пределами сидера (не публикуется в галерею как секрет).

События и обмен

СобытиеКогдаPayload
ApiTokenIssuedВыпуск нового токенаtoken_id, user_id, abilities[]
ApiTokenRevokedОтзыв токенаtoken_id, user_id
ApiTokenUsedFromNewIpПервое использование с нового IPtoken_id, ip

Провайдер facts для cms/notifications-bus: ApiTokenUsedFromNewIp резолвится в уведомление владельцу через контракт notification-bus (канал по preferences). Своих provides-контрактов и фильтров FilterBus нет. Слушает только собственные запросы Sanctum-middleware (перехват использования токена для журнала).

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
Ядро: пользователи (users)сервис-вызов (core-contracts)inТокен привязан к владельцу; список токенов и abilities читаются через модель User/relation Sanctum, без raw SQL
Ядро: настройки (SettingsStore)сервис-вызов (core-contracts)inЧтение группы api-tokens из кеша группы (0 запросов на горячем пути)
Ядро: аутентификация (Sanctum)сервис-вызов (core-contracts)in/outПроверка токена и auth:sanctum — часть ядра; модуль только слушает факт использования (журнал) и добавляет rate-limit поверх
cms/notifications-bus (suggests)provides-контракт (NotificationDispatch)outApiTokenUsedFromNewIp → уведомление владельцу; модуль выключен → log-fallback, письмо не уходит
cms/audit (если включён)событиеoutСлушает ApiTokenIssued / ApiTokenRevoked для журнала действий; api-tokens не знает о его существовании

Фоновая работа

Очередь api-tokens: джоба PruneExpiredTokens (идемпотентна) по расписанию через ScheduleRegistrar ядра, частота — из usage_log_retention_days. Джоба RevokeInactiveTokens (идемпотентна) — по тому же расписанию, отзывает токены без активности дольше api-tokens.auto_revoke_inactive_days (см. «Настройки»), 0 — джоба выключена. Отправка уведомления о новом IP — асинхронно, через cms/notifications-bus.

Ранбук (типовые инциденты):

СимптомЧто проверитьКоманда
Токены массово не проходят аутентификациюКеш группы api-tokens в SettingsStore (не протух ли и не содержит ли стейл-значение issuance_enabled), доступность Sanctum-middleware ядраcms:api-tokens:doctor --json
Подозрение на утечку токенаСписок активных токенов пользователя, last_ip/last_used_at в cms_api_token_meta; массовый отзыв с карточки пользователяApiTokenRevoked в аудите (если cms/audit включён) после массового отзыва
Журнал использования не растёт / очередь api-tokens отстаётДлина очереди api-tokens, воркер запущен, PruneExpiredTokens/RevokeInactiveTokens не падают на серединеcms:api-tokens:doctor --json, вручную cms:api-tokens:prune-expired --json

Бэкап/рестор: в бэкап попадают обе таблицы модуля — cms_api_token_meta и cms_api_token_usage_log (журнал ротируется по retention, но на момент бэкапа входит целиком). Денормализованных агрегатов и поисковых индексов модуль не хранит — last_ip/last_used_at в cms_api_token_meta восстанавливаются вместе со строкой, отдельной командой после рестора пересчитывать нечего.

Производительность и кеш

Ожидаемые объёмы (студийный проект, не highload): активных пользователей API — десятки-сотни; токенов на пользователя — 1–5 (основной сайт, мобильное приложение, интеграция с CRM, личный доступ). cms_api_token_usage_log: при активной интеграции с частотой 10–50 запросов/мин это 15–70 тыс. строк/сутки на токен, при retention 30 дней (дефолт) — до нескольких миллионов строк в журнале суммарно. Таблица спроектирована как append-only с BRIN-индексом по used_at именно под этот объём.

Горячие пути и бюджет запросов:

  • проверка токена на каждом API-запросе (auth:sanctum) — путь ядра, api-tokens не добавляет к нему синхронных запросов сверх необходимого;
  • запись в cms_api_token_usage_log происходит на каждом аутентифицированном запросе — синхронный INSERT на каждый вызов не проходит бюджет горячего пути; пишется асинхронно через очередь api-tokens либо буферизуется (батч-инсерт раз в N секунд/строк); обновление last_ip/last_used_at в cms_api_token_meta — тем же асинхронным обработчиком, отдельного синхронного UPDATE на горячем пути нет;
  • rate-limit per-токен — атомарный инкремент в Redis (тот же механизм, что общий API rate-limit ядра), запросов к БД не делает;
  • список токенов пользователя и виджет «токены на истечении» — низкочастотные admin-запросы, отдельного бюджета не требуют.

Критичные индексы (см. «Модель данных»): token_id на cms_api_token_meta (constrained()->index()); составной token_id + used_at на cms_api_token_usage_log — под keyset-выборку журнала конкретного токена; BRIN по used_at — под очистку по retention; expires_at на cms_api_token_meta — под виджет «токены на истечении» и джобу PruneExpiredTokens (оба фильтруют по этому полю — без индекса полный скан таблицы). RevokeInactiveTokens фильтрует по last_used_at — нативному полю Sanctum на personal_access_tokens (читается через relation, без raw SQL); отдельный индекс не заводится — объём таблицы (десятки-сотни пользователей × 1–5 токенов) не требует его при текущих масштабах.

Что кешируется: настройки группы api-tokens — стандартным кешем SettingsStore ядра (канон §6 стандарта). Список токенов и журнал использования — не кешируются: данные низкочастотные, но отзыв токена обязан быть виден мгновенно (кеш создал бы окно, где отозванный токен выглядит активным в UI). Собственных тегов CacheTags нет — на page-cache модуль не влияет. Rate-limit — не «контентный» кеш, а счётчик с TTL окна (60 сек), отдельная инвалидация не нужна (протухает сам).

Безопасность

Filament — FormRequest на выпуск токена (whitelist abilities из реестра permissions включённых модулей, см. «Модель данных»; abilities не может быть пустым массивом); API /api/v1/admin/api-tokens/* — авторизация по permissions; Sanctum-middleware — только чтение метаданных для журнала, не тела запроса. Rate-limit per-токен применяется поверх общего лимита API и не обходится параллельными запросами (атомарный инкремент, не read-then-write). Секретов в открытом виде не хранит — Sanctum хеширует токен, plaintext доступен только в ответе на выпуск.

Векторы атак и защита:

ВекторЗащита
Подбор токена (brute-force)Токен Sanctum — случайная строка достаточной энтропии; rate-limit на границе аутентификации (ядро) не даёт перебирать значения
Утечка через логи/URLТокен передаётся только в заголовке Authorization, никогда в query-string; журнал использования хранит route/ip/used_at, не значение токена; логирующие middleware обязаны редактировать Authorization
Replay (украденный токен работает, пока не отозван)Короткий дефолтный TTL (90 дней), уведомление о новом IP, ручной и массовый отзыв
Timing-атака на сравнение хешаСравнение — штатный hash_equals Sanctum (константное время); модуль не реализует собственного сравнения токенов

Права: api-tokens.view, api-tokens.manage, api-tokens.manage.own.

Матрица ролей:

РольПросмотр своихПросмотр чужихВыпускОтзыв (свой/чужой)Массовый отзыв
Пользователь (api-tokens.manage.own)✅ (себе)✅ / —
Менеджер/админ (api-tokens.manage)✅ / ✅
Studio✅ / ✅

UX-требования

Админ (роль api-tokens.manage):

  • пустой список токенов в системе — заглушка с подсказкой «Токены ещё не выпускались» и кнопкой выпуска;
  • массовые действия на списке: отзыв нескольких выбранных токенов, отзыв всех токенов пользователя разом (например при компрометации аккаунта) — отдельная кнопка на карточке пользователя, не только на списке токенов;
  • ошибки на человеческом языке: «Токен уже отозван», «Достигнут лимит активных токенов () — отзовите неиспользуемый», а не сырое исключение;
  • отзыв токена — необратимая операция: подтверждающий диалог с явным текстом («Токен перестанет работать немедленно, отменить нельзя»); массовый отзыв — подтверждение с указанием количества затронутых токенов.

Пользователь, управляющий своими токенами (api-tokens.manage.own):

  • plaintext токена показывается один раз, сразу после выпуска, с кнопкой «скопировать» и предупреждением «сохраните сейчас — повторно мы его не покажем»; закрытие диалога без подтверждения копирования — отдельный явный шаг;
  • список токенов показывает только label, abilities, expires_at, last_used_at — без plaintext;
  • форма выпуска сохраняет введённые label/abilities при ошибке валидации, не сбрасывает форму.

Крайние случаи и типовые баги

  • Токен показан при выпуске, диалог закрыт без копирования → повторный просмотр plaintext невозможен (Sanctum его не хранит) — только выпуск нового токена и отзыв старого;
  • Ротация токена → дефолт: выпуск нового + немедленный отзыв старого (без grace-периода), минимизирует риск; опционально rotation_grace_hours > 0 оставляет старый токен рабочим до истечения окна для бесшовной замены в интеграциях;
  • Отзыв всех токенов пользователя разом (компрометация аккаунта) → одна операция инвалидирует все токены пользователя, каждый порождает ApiTokenRevoked, факт фиксируется в cms/audit (если включён) единым действием;
  • Истёкший токен → 401 с машиночитаемым code: token_expired в конверте ошибок (не просто 401 без пояснения), отдельные коды token_revoked / token_missing для остальных причин отказа аутентификации. Поле code — обязательная часть конверта ошибок ядра ({message, code, errors}) по ревизии ядра 14.07.2026, п.1; значения token_expired/token_revoked/token_missing — стабильные строки модуля, консистентные с примером ревизии;
  • Ability токена ссылается на выключенный модуль → permission выключенного модуля не зарегистрирован в Gate → проверка ability возвращает отказ (403), не исключение (500); токен остаётся рабочим для оставшихся abilities;
  • Токен не использовался дольше auto_revoke_inactive_daysRevokeInactiveTokens отзывает его автоматически (тот же эффект, что ручной отзыв), порождает ApiTokenRevoked; владелец не получает отдельного уведомления сверх записи в журнале аудита (см. «Фоновая работа») — интеграция, простаивающая дольше срока, должна перевыпустить токен; auto_revoke_inactive_days = 0 выключает поведение полностью;
  • Гонка: параллельные запросы с одним токеном превышают rate-limit → атомарный инкремент в Redis (не read-then-write) не даёт превысить лимит даже при полностью одновременных запросах;
  • Двойной клик на «Выпустить токен» → два токена не создаются: Filament блокирует повторный сабмит на UI-уровне, API — по Idempotency-Key, обязательному на POST /api/v1/admin/api-tokens (см. «API») по ревизии ядра 14.07.2026, п.8 — идемпотентность явно распространена на выпуск токенов/ключей;
  • PruneExpiredTokens падает на середине → идемпотентна: удаление чанками по expires_at < now(), повторный прогон просто не находит уже удалённые строки, частичное выполнение не портит состояние;
  • cms/notifications-bus выключен → уведомление о новом IP не уходит: NotificationDispatch работает в log-fallback (факт остаётся в логе модуля, владелец токена узнаёт только из журнала использования, не из письма);
  • Пустой список токенов у нового пользователя → не ошибка, штатное пустое состояние с подсказкой (см. «UX-требования»);
  • abilities[] пуст при выпуске → отклоняется 422: токен без единой ability — «живой», но бесполезный и вводящий в заблуждение credential, FormRequest требует минимум одну ability;
  • Изменение default_rate_limit в настройках → не влияет на уже выпущенные токены: rate_limit фиксируется в cms_api_token_meta на момент выпуска, новый дефолт применяется только к новым токенам (задокументированное поведение, не баг).

Донорский код

Что взятьПуть
Управление токенами, журнал использованияfreelance/project/src/app/ (Sanctum-обвязка)

Легаси-импорт (§16 стандарта): не применимо. У модуля нет исторических данных для миграции — Sanctum-токены выпускаются заново на новой платформе, старые токены донора не переносятся по соображениям безопасности (чужой plaintext недоступен, переносить хеш смысла нет: он привязан к алгоритму и personal_access_tokens целевой системы). cms:api-tokens:import-legacy не реализуется; при переезде клиента donor-пользователи просто выпускают токены заново через Filament.

Тесты и приёмка

  • [ ] Контрактные тесты: отозванный токен немедленно теряет доступ;
  • [ ] health-чек проверяет наличие просроченных неотозванных токенов (предупреждение);
  • [ ] деградация при выключении — Sanctum работает штатно, без UI-управления;
  • [ ] rate-limit per-токен не даёт превысить лимит даже при параллельных запросах;
  • [ ] нет N+1 при выводе списка токенов с последним использованием;
  • [ ] журнал использования не хранится бессрочно — очищается по retention;
  • [ ] plaintext токена возвращается только в ответе на выпуск, не логируется и не хранится в БД — повторный просмотр невозможен;
  • [ ] ротация токена: при rotation_grace_hours = 0 старый токен недействителен мгновенно, при > 0 — до истечения grace-периода;
  • [ ] массовый отзыв всех токенов пользователя инвалидирует их одной операцией;
  • [ ] истёкший/отозванный/отсутствующий токен возвращают разные значения code (token_expired/token_revoked/token_missing) при одинаковом HTTP 401, конверт соответствует ревизии ядра 14.07.2026;
  • [ ] ability токена на выключенный модуль не роняет проверку (403, не 500);
  • [ ] параллельные запросы с одним токеном не превышают rate-limit (тест на гонку, атомарный инкремент);
  • [ ] повторный выпуск токена с тем же Idempotency-Key не создаёт дубликат;
  • [ ] PruneExpiredTokens идемпотентна при повторном прогоне после сбоя на середине чанка;
  • [ ] выключенный cms/notifications-bus не роняет выпуск/использование токена — уведомление уходит в log-fallback;
  • [ ] выпуск сверх max_active_per_user отклоняется понятной ошибкой, не 500;
  • [ ] abilities[] пустым массивом отклоняется на этапе валидации (422);
  • [ ] abilities при выпуске принимает только permissions из реестра включённых модулей (whitelist динамический, не хардкод) — попытка указать permission отсутствующего в реестре модуля отклоняется 422;
  • [ ] ответ на список токенов содержит last_ip, значение совпадает с последней строкой cms_api_token_usage_log для того же токена (нет рассинхрона денормализации);
  • [ ] RevokeInactiveTokens отзывает токен, не использовавшийся дольше auto_revoke_inactive_days, и не трогает активные; auto_revoke_inactive_days = 0 — джоба не отзывает ничего (тест на оба режима);
  • [ ] контрактный набор cms-testing зелёный, пакет протестирован в testbench-изоляции;
  • [ ] feature-тест на каждый роут API; тестовая БД только api-tokens_test, migrate:fresh/refresh/reset запрещены.

Внутренняя база знаний студии