Skip to main content
Панель администратора — это консоль оператора по адресу /admin. Её API живёт на /v1/panel и аутентифицируется cookie сессии, а не bearer-токеном APP_OPERATOR_TOKEN, который используется для /v1/admin. Эти две поверхности разделены сознательно: у них разные модели доверия, а общий префикс позволил бы одной унаследовать middleware другой.

Настройка первого владельца

В свежей установке нет ни одного аккаунта оператора, поэтому войти пока некому. При первом запуске API создаёт одноразовый токен установки и пишет его в свой лог:
Откройте /admin/setup, вставьте токен и создайте первого владельца. Токен действует 30 минут и может быть использован один раз. Путь начальной настройки закрывается навсегда, как только появляется хотя бы один аккаунт оператора. Токен, утёкший до этого момента, после уже нельзя использовать, а запрос нового токена аннулирует выданный ранее.
Токен установки даёт права владельца над всей установкой. Читайте его прямо из лога сервера, а не пересылайте лог в чат или в тикет.

Роли и права доступа

Роли встроены в приложение. Каталог прав доступа и сопоставление ролей с правами скомпилированы в приложение, поэтому правка базы данных не может расширить доступ; база хранит только то, какие роли есть у оператора. Только владелец может создать или пригласить оператора, изменить назначенные роли или выдать роль владельца. Это ограничение оставлено за владельцем, чтобы скомпрометированная сессия администратора не могла создать себе второй аккаунт или расширить собственные привилегии. В установке всегда остаётся хотя бы один активный владелец. Отзыв роли у последнего владельца или его блокировка отклоняются, вместо того чтобы оставить панель без администрирования. Права доступа проверяются в Go API на каждом запросе. Панель отрисовывается по тому же набору прав, который получает, поэтому скрытие элемента управления — это только представление: обращение к скрытому маршруту ничего не даёт. В v0.7 в каталог добавлены risk, gifts и goods. Что они охватывают и почему это отдельные возможности, описано в разделе Операции администратора.

Приглашение операторов и смена ролей

Владелец добавляет оператора на странице /admin/operators кнопкой Пригласить оператора: адрес почты и одна или несколько ролей. API отвечает одноразовой ссылкой /admin/invite/<token>, и панель показывает её один раз с кнопкой копирования. В Omniflow нет отправки почты, поэтому владелец передаёт ссылку сам — так же, как передал бы любые другие учётные данные.
  • Ссылка срабатывает один раз и истекает через 72 часа. Хранится только SHA-256 её токена; показать её повторно панель не может.
  • Повторное приглашение того же адреса отзывает прежнюю ссылку, поэтому потерянная или перехваченная ссылка заменяется, а не остаётся рабочей.
  • Адрес, у которого уже есть учётная запись оператора, пригласить нельзя — вместо этого измените роли этого оператора.
  • Ожидающие приглашения показывают все неиспользованные ссылки, отмечают истёкшие и позволяют отозвать любую.
Приглашённый открывает ссылку вне оболочки панели, видит адрес и роли, с которыми его пригласили, и задаёт отображаемое имя и пароль (не менее 12 символов, с тем же индикатором надёжности, что и на странице безопасности). После этого он сразу входит в систему, и ему предлагается /admin/security, чтобы подключить двухфакторную аутентификацию или ключ доступа; установка не требует ни того, ни другого. Изменить роли в строке оператора открывает редактор ролей. API проверяет эти правила при каждом изменении, а редактор заранее отключает то, что будет отклонено:
  • Никто не может менять собственные роли.
  • Каждая добавляемая или снимаемая роль должна содержать только те права, которые уже есть у того, кто вносит изменение; выдать или снять роль владельца может только владелец.
  • У оператора остаётся хотя бы одна роль; закрыть доступ можно блокировкой.
  • В установке остаётся хотя бы один активный владелец. Изменения ролей, которые могут убрать владельца, выполняются последовательно, поэтому два владельца не могут одновременно понизить друг друга.
  • Сохранение неизменённого набора ничего не делает и не записывается.
Таблица Роли и права на той же странице читается из скомпилированного каталога, который применяет API, — по строке на раздел. Выдача, отзыв и принятие приглашения записываются в аудит как admin.invitation.created, admin.invitation.revoked и admin.invitation.accepted; изменение ролей — как admin.account.roles_changed с наборами до и после.

Сессии

Хранится только SHA-256-дайджест токена сессии, поэтому ни чтение базы данных, ни офлайн-резервная копия не дают пригодного cookie. Одновременно работают два срока истечения:
  • Бездействие — 30 минут, сдвигается вперёд на каждом аутентифицированном запросе.
  • Абсолютный — 12 часов, никогда не продлевается.
Сессия, которую фоновая вкладка держит активной, всё равно завершается на абсолютном горизонте. Токены к тому же ротируются каждые 15 минут, что ограничивает, как долго токен, перехваченный из лога или с прокси, остаётся пригодным для повторного использования. Каждый оператор может просмотреть свои активные сессии на /admin/security, завершить любую из них или выйти на всех остальных устройствах. Смена пароля завершает все прочие сессии; завершённый сброс пароля завершает их все. На том же экране регистрируются и отзываются passkey. Passkey выполняет вход сам по себе: аутентификатор доказывает владение и проверяет человека, поэтому экран входа пускает сразу — без пароля и без кода. У каждого ключа есть название, которое выбирает оператор: именно оно позволяет через несколько месяцев отозвать нужный. В списке также видно, когда каждый ключ использовался в последний раз. Ключи не заменяют пароль, поэтому удаление последнего никого не блокирует. Кнопки нет, если в установке не задан APP_PUBLIC_URL, к которому привязывать учётные данные, или если браузер не умеет выполнять эту процедуру. См. Безопасность. У поля нового пароля есть индикатор надёжности. Он оценивает энтропию по алфавиту, который пароль действительно использует, а затем вычитает за шаблоны, из-за которых этот алфавит — обман: слишком мало разных символов, последовательность с клавиатуры или из алфавита, одно неразрывное слово, — и называет одну-две вещи, которые помогут сильнее всего. Это намеренно не чек-лист классов символов: «одна заглавная, одна цифра, один символ» поощряет Password1!, который входит в первую тысячу попыток любой настоящей атаки, и наказывает длинную парольную фразу, которая по-настоящему трудна. Индикатор никогда не отклоняет пароль. Минимальная длина — правило API, и оно проверяется там; индикатор, который бы блокировал, был бы вторым местом, решающим, какой пароль допустим.

Двухфакторная аутентификация

TOTP следует RFC 6238: 6-значные коды с шагом 30 секунд, и работает с любым стандартным приложением-аутентификатором. Секреты запечатываются ключом APP_DATA_ENCRYPTION_KEY перед сохранением. Подключение состоит из двух шагов. Добавление секрета сохраняет его неподтверждённым; он не удовлетворяет ни одну проверку при входе, пока не проверен первый код. Брошенное на полпути подключение оставляет аккаунт ровно таким, каким он был. На экране подключения рядом с секретом показан QR-код. Он рисуется в браузере из URI otpauth://, который отдаёт API, и никогда не запрашивается как картинка: этот URI несёт сам секрет, поэтому запрос к любому image-эндпоинту — включая собственный эндпоинт приложения — положил бы учётные данные в строку запроса, в лог прокси и в кэш. Секрет печатается и текстом, потому что аутентификатор, работающий на том же устройстве, не может сфотографировать экран, на котором находится. Подтверждение подключения выдаёт десять одноразовых кодов восстановления, показываемых один раз. Хранятся только их дайджесты. Перегенерация набора аннулирует все неиспользованные коды. Отключение двухфакторной аутентификации требует повторного ввода пароля, потому что снятие второго фактора — ровно то, что попытается сделать злоумышленник с перехваченной сессией.

Защита входа

  • Вход отвечает одинаково при неверном пароле, неизвестном адресе и отключённом аккаунте, а неизвестный адрес проверяется по подставному хешу, поэтому стоит столько же. Перебрать аккаунты операторов таким образом нельзя.
  • Пять последовательных неудач блокируют аккаунт, и с каждой следующей неудачей ожидание удваивается вплоть до одного часа.
  • Запросы ограничиваются по частоте на адрес и на исходный адрес, поэтому не получится задавить ни отдельный аккаунт, ни множество аккаунтов веером.
  • Сброс пароля всегда отвечает 202 независимо от того, соответствовал ли адрес какому-либо аккаунту.
Транспорта электронной почты для операторов нет, поэтому публичный запрос сброса не выдаёт токен: он записывает запрос и публикует уведомление в теме 🔐 Admin security. Токен выдаётся только на сервере командой admin reset-password исполняемого файла API, которая печатает его в этот терминал:
См. Безопасность.

Уведомления о безопасности

События, значимые для безопасности, публикуются в топик 🔐 Admin security в группе операторов в Telegram, рядом с потоками покупок и инцидентов:
  • вход с адреса, который аккаунт ранее не использовал
  • смена пароля или завершённый сброс пароля
  • отключение двухфакторной аутентификации
  • выдача роли владельца
  • любое другое изменение ролей оператора
  • принятие приглашения — то есть в панель теперь может входить новый человек
Уведомление называет событие и аккаунт оператора и больше ничего. Адрес, токен сессии и IP клиента остаются вне группового чата; эти детали живут в журнале аудита, за правами доступа, которые им управляют. Уведомления записываются в той же транзакции, что и изменение, которое они описывают, поэтому ни одно из них не может объявить о том, что было откачено, и они дедуплицируются, поэтому повторённая транзакция даёт одно сообщение.

Командная палитра и поиск записей

Ctrl+K (⌘K на Mac), поле поиска в шапке или значок поиска на телефоне открывают командную палитру. Она переходит на любой экран, доступный оператору, запускает частые действия (пригласить оператора, настроить двухфакторную аутентификацию) и находит записи. Поиск записей обращается к GET /v1/panel/search?q= и показывает до пяти результатов в каждой группе — с краткой строкой описания и тем, по чему найдено совпадение: Группа, которую роли оператора не позволяют читать, не запрашивается и не попадает в ответ, поэтому палитра никогда не покажет запись, которую не откроет её ссылка. Почта сравнивается только точно — через ключевой отпечаток, который хранится рядом с зашифрованным адресом, — и сам адрес не возвращается. Omniflow не хранит имена пользователей Telegram (@username), поэтому запрос, начинающийся с @, не находит клиентов, и палитра об этом сообщает; используйте числовой Telegram ID. Каждый поиск — это обращение к индексу: префикс ID превращается в диапазонное сканирование по первичному ключу, а для тем обращений используется триграммный индекс из расширения pg_trgm, которое включает миграция (это доверенное расширение, поэтому владелец базы может создать его без прав суперпользователя). Поиск ограничен 60 запросами в минуту на оператора и, как и любое другое чтение в панели, не записывается в журнал аудита.

Журнал аудита

Каждое действие аутентификации, авторизации и настройки добавляется в audit_events, где доступно только чтение. Каждая строка несёт субъекта действия, объект, действие, категорию, исход, причину, идентификатор запроса, отметку времени и безопасные метаданные до/после. События классифицируются по двум осям, по которым панель фильтрует и экспортирует:
  • Категория — authentication, authorization, configuration, customer, financial, support, marketing, system
  • Исход — success, failure, denied
Отказ по правам доступа не только выполняется, но и записывается: оператор, раз за разом тянущийся к тому, чего ему нельзя, — именно то, что ищет разбор аудита. Просматривать журнал можно на /admin/audit. Фильтры хранятся в URL, поэтому представление можно сохранить в закладки и передать коллеге. Любой, у кого есть audit.export, может скачать отфильтрованный журнал в CSV.

Вход через OIDC

OIDC — это конфигурация, а не код: укажите документ обнаружения и учётные данные клиента в разделе Settings → OIDC, и ничего специфичного для конкретного провайдера в приложении нет. Подходит всё, что говорит на OpenID Connect Discovery, а установка без настроенного провайдера аутентифицируется одними паролями. Используется поток authorization code с PKCE. PKCE применяется, хотя это конфиденциальный клиент, потому что он привязывает код к исходному запросу: код, перехваченный на редиректе, не может быть использован кем-то другим. Состояние против подделки запроса и верификатор PKCE передаются в короткоживущем зашифрованном cookie, а не в базе данных, поэтому брошенный вход не оставляет после себя ничего. Каждый ID-токен проверяется по опубликованным провайдером JWKS, издателю и аудитории, прежде чем ему начинают доверять. Привязанная идентичность записывает издателя, у которого её привязали, и совпадает только с токеном от того же издателя, поэтому правка издателя провайдера не может передать его операторов кому-то другому. Изменять провайдера операторов может только владелец (settings.write плюс admins.roles), а провайдера с привязанными операторами вообще нельзя перевести на другого издателя — см. Вход через OIDC. Тем, к какому аккаунту доберётся субъект, управляют две политики:
  • Требуется подтверждённый адрес электронной почты (по умолчанию включено). Провайдер может утверждать адрес, который он не подтверждал; требование этого утверждения не даёт ему выписать доступ к аккаунту, который ему не принадлежит.
  • Автоматическое создание аккаунтов (по умолчанию выключено). Субъект, не сопоставленный ни с одним привязанным аккаунтом, получает отказ. Со включённым автосозданием субъект, чей адрес ещё не занят, получает новый аккаунт в настроенной роли. Адрес, который уже принадлежит оператору, никогда не присваивается молча — привязка должна быть осознанным действием того, кто уже вошёл в систему. Роль владельца не может быть выдана автосозданием никогда.
URI редиректа, который нужно зарегистрировать у провайдера:

Конфигурация

Работа за обратным прокси

X-Forwarded-For подделывается тривиально, поэтому он учитывается только тогда, когда непосредственный узел-отправитель перечислен в APP_TRUSTED_PROXIES. Оставьте переменную пустой, когда API выставлен наружу напрямую — тогда используется адрес сокета, и это правильно. Адрес питает ограничение частоты запросов и журнал аудита, поэтому ошибка здесь важна в обе стороны: не доверять ничему — и все запросы за прокси делят один адрес; доверять слишком многому — и злоумышленник выбирает свой адрес сам.
APP_ADMIN_COOKIE_SECURE=false предназначено только для локальной разработки по обычному HTTP. Без Secure cookie сессии может быть перехвачен из любого незашифрованного запроса к тому же хосту.

Заголовки безопасности

Каждый ответ панели несёт nosniff, X-Frame-Options: DENY, строгий Content-Security-Policy с frame-ancestors 'none', Referrer-Policy: no-referrer и Cache-Control: no-store. Cookie сессии называется __Host-omniflow_admin, когда несёт Secure: это привязывает её к текущему origin — браузер отвергает такую cookie с атрибутом Domain, поэтому соседний поддомен не может установить или перезаписать сессию оператора. Этот отказ безусловен и распространяется в том числе на саму доставку cookie. Браузер отвергает cookie с префиксом __Host-, отправленную без Secure, целиком, а не считает её просто более слабой. Поэтому при APP_ADMIN_COOKIE_SECURE=false такое имя делало бы вход не небезопасным, а невозможным: cookie отбрасывалась бы при получении, и каждая попытка возвращала бы на экран входа. Префикс поэтому следует за атрибутом, от которого зависит: cookie называется __Host-omniflow_admin, когда она Secure, и omniflow_admin, когда нет. В продакшене имя такое же, как и всегда; отличается только локальный стек по обычному HTTP, где префикс всё равно был бы обещанием, которое cookie не может выполнить. Каждый небезопасный метод дополнительно требует CSRF-токен, привязанный к сессии и возвращаемый в X-CSRF-Token; он выводится из секрета, хранящегося вместе с сессией, поэтому токен, выпущенный для одной сессии, нельзя повторно использовать в другой.