Безопасность¶
Модуль CM Ocean Loyalty построен на модели deny-by-default: весь API закрыт
аутентификацией, и доступ к каждой операции дополнительно проверяется по роли.
Бэкенд loyalty-billing работает как OAuth2 resource server: он не управляет
паролями и сессиями, а доверяет токенам, выпущенным внешним сервером авторизации
Keycloak, и проверяет их при каждом запросе.
Контур безопасности¶
flowchart LR
UI["Core UI / клиент"] -->|Bearer JWT| API["loyalty-billing\n(resource server)"]
KC["Keycloak\n(сервер авторизации)"] -->|выпуск JWT| UI
API -->|проверка подписи / issuer| KC
| Свойство | Значение |
|---|---|
| Тип компонента | OAuth2 resource server (Spring Security) |
| Сервер авторизации | Keycloak |
| Формат токена | JWT (Bearer) |
| Управление сессиями | Stateless — серверная сессия не создаётся |
| Защита от CSRF | Отключена (для stateless API не требуется) |
| Модель доступа | Deny-by-default + method-level @PreAuthorize |
Аутентификация: проверка JWT¶
Каждый запрос к защищённым ресурсам должен содержать заголовок с access-токеном:
Сервис проверяет подпись и срок действия токена, а также его издателя (issuer). Адрес издателя задаётся только через окружение и не хранится в коде:
| Параметр | Назначение |
|---|---|
spring.security.oauth2.resourceserver.jwt.issuer-uri |
URI издателя (issuer) Keycloak; источник публичных ключей для проверки подписи |
Stateless
Сервис не хранит серверное состояние сессии — каждый запрос аутентифицируется заново по JWT. Каждый запрос самодостаточен и аутентифицируется по предъявленному токену. Это упрощает горизонтальное масштабирование и поведение под балансировщиком.
Источник ролей в токене¶
Роли извлекаются из клейма resource_access[<client-id>].roles JWT и имеют
префикс продукта LOYALTY_. Идентификатор клиента (client-id), по которому
читаются роли, задаётся конфигурацией модуля. Интерактивный (UI) клиент использует
поток Authorization Code; сервисные (не-UI) клиенты — Client Credentials, набор
прав при этом тот же.
Авторизация: deny-by-default и @PreAuthorize¶
Правило доступа двухуровневое:
- Транспортный уровень. Всё, что не входит в явный список публичных путей,
требует валидного JWT (
anyRequest().authenticated()). - Уровень операции. На каждый метод API навешена аннотация
@PreAuthorizeс требуемой ролью. Наличие валидного токена недостаточно — у субъекта должна быть нужная рольLOYALTY_*.
Роли сгруппированы по доменам и разделяют чтение (*_READ) и запись (*_WRITE):
| Группа ролей | Назначение |
|---|---|
LOYALTY_CUSTOMER_READ / LOYALTY_CUSTOMER_WRITE |
Клиенты лояльности; запись — создание, правка профиля (в MDM), слияние клиентов |
LOYALTY_ACCOUNT_READ / LOYALTY_ACCOUNT_WRITE |
Счета и баланс баллов |
LOYALTY_PROFILE_READ |
Профиль клиента (MDM identity + лоялти) |
LOYALTY_ACCRUAL_WRITE |
Начисление баллов |
LOYALTY_REDEMPTION_WRITE |
Списание: резерв (hold), commit/cancel, прямое списание |
LOYALTY_OPERATION_WRITE |
Денежные операции: пополнение, списание, перевод, обмен |
LOYALTY_CURRENCY_READ / LOYALTY_EXCHANGE_RATE_READ |
Валюты и курсы обмена |
LOYALTY_PROMOTION_READ / LOYALTY_PROMOTION_WRITE |
Каталог механик и акции; экспорт-импорт акций между контурами; запись — создание, правка и отправка акции на согласование (без запуска) |
LOYALTY_PROMOTION_ACTIVATE |
Запуск акции (одиночный и массовый) — нужен всегда, независимо от того, включено ли согласование |
LOYALTY_PROMOTION_APPROVE |
Решение по согласованию запуска акции (согласовать / отклонить); решение по своей же отправке отклоняется |
LOYALTY_PROGRAM_CONFIG_READ / LOYALTY_PROGRAM_CONFIG_WRITE / LOYALTY_PROGRAM_CONFIG_ACTIVATE |
Конфигурации программы лояльности; активация — отдельное право, запись его не подразумевает |
LOYALTY_CLIENT_PROFILE_ACTIVATE / LOYALTY_CLIENT_PROFILE_DELETE |
Активация и удаление версий настройки карточки клиента (правка самой настройки — LOYALTY_DICTIONARY_WRITE) |
LOYALTY_SEGMENT_READ / LOYALTY_SEGMENT_WRITE |
Экран «Сегменты клиентов» и журнал заливок; запись — архивация, возврат из архива и удаление сегмента |
LOYALTY_SEGMENT_INTAKE |
Приём сегментов из внешней системы — только сервисной учётной записи этой системы: заливка меняет, кому начисляют все сегментные акции |
LOYALTY_NOTIFICATION_READ / LOYALTY_NOTIFICATION_WRITE |
Разбор очереди исходящих уведомлений клиентам |
LOYALTY_CITY_*, LOYALTY_STORE_*, LOYALTY_MERCHANT_*, LOYALTY_PRODUCT_CATALOG_* (_READ / _WRITE) |
Справочники со своим API, отдельные от общих справочников: города, торговые точки, мерчанты, каталог товаров (/v1/admin/cities, /v1/admin/stores, /v1/admin/merchants, /v1/admin/product-categories, /v1/admin/product-items). Экранов для них в веб-интерфейсе пока нет — в разделе «Справочники» их строки неактивны |
LOYALTY_ACHIEVEMENT_READ / LOYALTY_ACHIEVEMENT_WRITE |
Справочник достижений (бейджей) |
LOYALTY_EVENT_WRITE |
Событие покупки (покупка → акция → начисление) |
LOYALTY_MAINTENANCE_WRITE |
Регламентное сгорание баллов |
LOYALTY_INTEGRATION_WRITE |
Приём чеков POS и начислений от модуля Promo |
LOYALTY_MONITORING_READ |
Мониторинг, аномалии, бизнес-день (чтение), сквозной журнал аудита |
LOYALTY_TIER_READ / LOYALTY_TIER_WRITE |
Уровни (тиры); запись — ручное назначение уровня |
LOYALTY_BUSINESS_DAY_WRITE |
Закрытие бизнес-дня |
LOYALTY_DICTIONARY_READ / LOYALTY_DICTIONARY_WRITE |
Справочники (НСИ); запись — LOCAL-записи и лимиты списания по разрезам |
LOYALTY_INTEGRATION_READ |
Резолюция карты лояльности на POS, проверка условий из RT Flow, проекция клиента для Check Scan, сегменты клиента для внешних систем — отдельно от _WRITE, чтобы её не получали скопом все пишущие интеграционные клиенты |
LOYALTY_INTAKE_WRITE / LOYALTY_INTAKE_READ |
Асинхронный приём команд (очередь command_inbox); чтение — разбор застрявших команд |
LOYALTY_EXCHANGE_READ / LOYALTY_EXCHANGE_WRITE |
Обмен данными: ленты изменений и выгрузки в DWH (отдельно от LOYALTY_INTEGRATION_READ — лента отдаёт реестр клиентов целиком, включая ПДн); запись — регламент (расписание, ручной запуск) |
LOYALTY_TAG_READ / LOYALTY_TAG_WRITE |
Справочник тегов клиента (свой экран); запись также заводит недостающие теги при создании клиента и импорте |
LOYALTY_CUSTOMER_TAG_WRITE |
Присвоение и снятие тегов у клиента — отдельно от справочника и от LOYALTY_CUSTOMER_WRITE |
LOYALTY_PROMOTION_LABEL_READ / LOYALTY_PROMOTION_LABEL_WRITE |
Справочник меток акций (свой экран); проставление метки на акцию — по LOYALTY_PROMOTION_WRITE |
Полный перечень ролей и соответствие ролей операциям — в разделе «Ролевая модель».
Гранулярность ролей денежных операций
Денежные операции (пополнение, списание, перевод, обмен) сейчас разделяют единую
роль LOYALTY_OPERATION_WRITE. При необходимости её можно аддитивно разбить
на отдельные роли по операциям без изменения остальной модели доступа.
Метод-уровневая проверка — на всех эндпоинтах, включая клиентский кабинет
Контур клиентского кабинета (/v1/b2c/**) и без того закрыт отдельной
deny-by-default цепочкой безопасности, но каждый из его эндпоинтов
дополнительно несёт @PreAuthorize — правило метод-уровневой проверки не
делает исключений ни для одного контура API.
Реестр сотрудников¶
Модуль ведёт реестр сотрудников app_user без обращения к административному API Keycloak: при
каждом запросе сотрудника из токена сохраняются идентификатор (sub), отображаемое имя, логин
(preferred_username), e-mail и роли LOYALTY_*. В реестр попадают только люди с ролями продукта:
клиентские токены без ролей, запросы клиентского кабинета /v1/b2c/** и сервисные учётные записи
интеграций (service-account-*) не записываются. Из реестра берутся имя и логин автора конфигурации
программы и её активации. Справочник GET /v1/users (роль LOYALTY_PROMOTION_WRITE, фильтр по праву и
поиск по имени) отдаёт только идентификатор и отображаемое имя — для выбора согласующих при отправке акции
на согласование через API; веб-интерфейс отправляет акцию без выбора согласующих, и согласовать её может
любой обладатель LOYALTY_PROMOTION_APPROVE, кроме отправившего; при выборе согласующих через API решение
принимают только они (иначе 403 approval_not_assigned). E-mail наружу не отдаётся. В списке появляются только
сотрудники, хотя бы раз входившие в продукт.
Публичные эндпоинты¶
Без аутентификации доступны только служебные эндпоинты эксплуатации и документация
API. Первые три вынесены на отдельный management-порт с базовым путём /
(то есть без префикса /actuator); документация API отдаётся на прикладном порту, том же, что
/v1/*, отдельным исключением из общего правила:
| Путь | Назначение | Порт |
|---|---|---|
/health, /health/** |
Проверки здоровья, в т.ч. пробы liveness/readiness для Kubernetes | management |
/metrics |
Метрики в формате Prometheus | management |
/info |
Служебная информация о сервисе | management |
/v3/api-docs, /v3/api-docs/** |
Спецификация OpenAPI | прикладной |
/swagger-ui/**, /swagger-ui.html |
Интерактивная документация Swagger UI | прикладной |
Совпадение матчеров с реальными путями проб
Так как management-порт использует базовый путь /, фактические адреса проб —
/health/liveness и /health/readiness, а Prometheus отдаётся на /metrics.
Эти пути входят в список публичных. Любое расхождение приведёт к ответу 401 на
пробах и сборе метрик, что нарушит работу k8s-проб и мониторинга.
Идемпотентность денежных операций¶
Все денежные операции идемпотентны: повторная отправка запроса с тем же ключом не приводит к повторному эффекту (двойному начислению или списанию), а возвращает результат исходной операции. Идемпотентность задаётся заголовком запроса:
Зачем это нужно
Заголовок защищает от дублирования при сетевых повторах и автоматических ретраях со стороны вызывающих систем (POS, модуль Promo, фронтенд). Клиент обязан генерировать уникальный ключ на каждую логическую операцию и переиспользовать тот же ключ только при ретрае той же операции.
Ключ действителен только для своей операции
Пространство ключей идемпотентности общее на все денежные ручки. Если тот же
ключ, что уже занят одной операцией (например, пополнением), приходит с
запросом к другой (например, списанию), сервис не подменяет ответ ответом
чужой операции — вызов отклоняется 422 idempotency_key_reused с указанием,
какой операцией ключ уже занят. Повторно использовать один и тот же ключ
можно только для ретрая той же логической операции.
Журнал идемпотентности хранит ответ операции ограниченное время (по умолчанию
7 суток, настройка LOYALTY_IDEMPOTENCY_RETENTION), после чего запись
регламентно убирается. Само по себе это не открывает окно для двойной
проводки: после истечения окна повтор бизнес-факта по тому же ключу проводки
всё равно ловит защита на уровне леджера (уникальность ссылки на проводку) —
журнал идемпотентности лишь экономит повторный расчёт и возвращает сохранённый
ответ, пока ключ ещё помнится.
CORS для встраивания в Core UI¶
CORS настроен узко: по умолчанию cross-origin-запросы запрещены (список разрешённых источников пуст). Разрешённые источники задаются через окружение — это нужно для встраивания UI лояльности в Core UI, когда браузер делает cross-origin XHR-запросы к API модуля.
| Параметр | Назначение |
|---|---|
LOYALTY_CORS_ALLOWED_ORIGINS |
Список разрешённых источников через запятую. Если пусто — cross-origin запрещён |
Разрешённые HTTP-методы фиксированы: GET, POST, PUT, PATCH, DELETE для основного
контура; для клиентского кабинета (/v1/b2c/**) список уже — только GET, POST, OPTIONS, по
набору операций, которые кабинет реально вызывает. Заголовки запроса разрешаются (*) только при
заданном списке источников.
Конфигурация под среду
Значение LOYALTY_CORS_ALLOWED_ORIGINS должно содержать точные источники
хоста Core UI для конкретной среды (dev/stage/prod). Избегайте избыточно
широких значений: пустой список безопаснее, чем неоправданно открытый CORS.
Клиентский кабинет (/v1/b2c/**) настраивается отдельным списком источников
(LOYALTY_B2C_CORS_ALLOWED_ORIGINS) — на случай, если кабинет встраивается на
другом домене, чем интерфейс сотрудника. Если для кабинета список не задан,
действует список сотрудника: контур, где оба интерфейса живут на одном домене,
задаёт один список, а не дублирует его дважды.
Журнал аудита¶
Модуль ведёт сквозной журнал аудита: он фиксирует не только изменяющие (write) вызовы API, но и обращения к персональным данным при чтении, а также даёт гарантии, что накопленные записи нельзя незаметно подделать, и передаёт поток событий во внешнюю систему мониторинга безопасности (SIEM) заказчика.
Состав записи¶
Каждая запись журнала несёт:
| Графа | Значение |
|---|---|
at |
Момент события |
actor |
Пользователь (имя из токена) или anonymous |
actorRoles |
Роли субъекта на момент действия — роль потом отзовут или выдадут, и по текущему состоянию каталога учётных записей это не восстановить |
sourceIp |
Адрес источника запроса — первый хоп X-Forwarded-For за ingress (то же определение адреса, что у ограничителя частоты, — иначе журнал и ограничитель расходились бы в ответе на вопрос «откуда пришёл запрос») |
action |
HTTP-метод; у доменных событий — код действия: PROMOTION_APPROVAL_SUBMIT / _APPROVE / _REJECT / _CANCEL — действия согласования запуска акции, включая автоматическую отмену согласования правкой акции; exchange-pair.rate.change — смена курса пары обмена валют |
target |
Путь запроса; у доменных событий — объект действия: promotion:<id> у согласования, пара валют с прежним и новым курсом у смены курса |
status |
HTTP-код ответа, включая отклонения 403/422/404 |
traceId |
Идентификатор запроса для корреляции с логами |
accessKind |
Вид обращения — см. ниже |
subjectExternalId |
Субъект персональных данных (только для просмотра карточки) |
itemCount |
Мера раскрытия: сколько записей отдал ответ |
criteria |
Имена параметров отбора без значений (значения фильтров сами бывают персональными данными — поиск идёт в том числе по телефону и e-mail) |
Виды обращения¶
| Вид | Когда пишется |
|---|---|
CHANGE |
Автоматически на каждый мутирующий запрос (POST/PUT/DELETE/PATCH) |
READ |
Просмотр карточки одного субъекта (карточка клиента, профиль, проекция Check Scan, служебный предпросмотр чужого кабинета в B2C) |
SEARCH |
Поиск/листинг, раскрывающий данные многих людей (каталог клиентов) |
EXPORT |
Выгрузка массива данных (лента обмена клиентами, выгрузка результатов пакетной операции, выгрузка самого журнала аудита) |
Обращение клиента к своим данным не аудируется
В журнал попадает только обращение сотрудника к чужим данным. Клиентский кабинет, отдающий человеку его собственные данные по его же токену, — не инцидент и не аудируется; иначе он на порядок чаще служебных обращений вытеснил бы из журнала именно то, ради чего журнал заводится.
Чтение и выгрузка¶
GET /v1/admin/audit отдаёт журнал страницей с фильтрами по пользователю,
периоду, виду обращения и субъекту ПДн; роль — LOYALTY_MONITORING_READ.
GET /v1/admin/audit/export?from=&to= выгружает журнал за период потоком в
CSV (разделитель ;, кавычки по RFC 4180, UTF-8 с BOM для Excel) — тот же
состав граф и тот же порядок, что на экране. Сама выгрузка попадает в журнал
как обращение вида EXPORT: она раскрывает разом всех клиентов, чьи карточки
упоминаются в выгруженных записях.
Защита от подделки¶
Журнал неизменяем: правка существующей записи запрещена всегда (на уровне базы), а удаление возможно только для записей, ещё не покрытых печатью.
Периодически (по умолчанию — каждые 5 минут) на очередной диапазон записей ставится печать — отпечаток (HMAC) содержимого диапазона плюс отпечаток предыдущей печати, то есть цепочка: подменить запись задним числом, не тронув все последующие звенья, нельзя. Печати не покрывают записи моложе минуты — верхняя граница печати обязана быть заведомо зафиксированной в базе.
GET /v1/admin/audit/integrity (роль LOYALTY_MONITORING_READ) отдаёт
результат проверки целостности цепочки печатей (проверка идёт и по
расписанию, ночью):
| Поле | Что значит |
|---|---|
intact |
Цела ли цепочка печатей: false — записи, уже покрытые печатью, изменены или удалены. О непокрытой части журнала не говорит ничего |
seals |
Число печатей в цепочке |
uncovered |
Записи после последней печати, которые исправная постановка печатей уже обязана была покрыть, — старше окна lag + 2 × tick (по умолчанию 11 минут). Моложе окна запись законно ждёт очередной печати и в счёт не входит |
broken |
Сломанные печати (sealId) и причина — изменение записей, удаление части журнала или разрыв связи с предыдущей печатью |
intact: true при пустой цепочке — не защита
При нуле печатей проверять нечего, и intact истинен — в том числе когда
подсистема выключена или печати ещё ни разу не ставились. Защищён ли
журнал, показывает uncovered: на исправном контуре — 0 в любой
момент; при пустой цепочке, в том числе при выключенных печатях, — весь
журнал старше окна; если печати перестали ставиться, число растёт от
проверки к проверке, хотя intact остаётся истинным, а broken —
пустым. Тот же счёт публикуется показателем
loyalty.audit.seal.uncovered, на нём заводится оповещение — см.
«Наблюдаемость».
Подсистема печатей выключена по умолчанию
Постановка печатей требует секрета (LOYALTY_AUDIT_SEAL_KEY) и без него не
включается: включённая без ключа подсистема отчитывалась бы «журнал цел»,
не будучи способной это установить.
Передача в SIEM заказчика¶
Поток событий журнала может передаваться во внешнюю SIEM заказчика (KUMA, MaxPatrol, ArcSight, QRadar и подобные) по syslog (RFC 5424) с полезной нагрузкой в формате CEF (есть и вариант JSON — для приёмников со своим разбором). Подключение сводится к настройке источника на стороне SIEM.
| Параметр | Назначение |
|---|---|
LOYALTY_SIEM_ENABLED |
Выключено по умолчанию — адрес приёмника знает только контур заказчика |
LOYALTY_SIEM_HOST / LOYALTY_SIEM_PORT |
Адрес приёмника |
LOYALTY_SIEM_TRANSPORT |
TCP (по умолчанию, с подтверждением доставки) или UDP (без гарантии) |
LOYALTY_SIEM_FORMAT |
CEF (по умолчанию) или JSON |
Гарантия доставки и курсор
Буфером служит сам журнал аудита — отдельной очереди для потока нет, хранится только курсор (докуда события уже переданы). Курсор двигается только после подтверждённой отправки: если приёмник недоступен, поток останавливается и продолжается с того же места после восстановления связи, без потери событий. Отклонённая попытка доступа передаётся с повышенной важностью — отказ разбирается, а успешный просмотр не привлекает внимания. За состоянием потока следит показатель отставания (сколько записей ещё не передано); растущее отставание означает, что события до SIEM не доходят.
Интеграционный периметр¶
Три меры защищают периметр интеграций и импорта данных от перегрузки, некорректных файлов и незаметного расхождения учёта.
Ограничение частоты запросов¶
Ограничение действует сплошным фильтром на все запросы (алгоритм GCRA, ключ —
субъект токена или IP-адрес). Правила заводятся в режиме OBSERVE (только
метрика и предупреждение в логе, без отказа) и переводятся в ENFORCE после
обкатки порога на реальном трафике; выигрывает первое совпавшее по пути и
методу правило.
| Правило | Что покрывает |
|---|---|
| Резолюция карты POS | GET /v1/integration/pos/cards/** |
| Проекция клиента для Check Scan | GET /v1/integration/checkscan/customers/** |
| Запись в интеграционный контур | POST/PUT/PATCH/DELETE /v1/integration/** |
| Чтение интеграционного контура | GET /v1/integration/** |
| События покупок | POST /v1/events/** |
| Массовые операции | приём пакетов ручных операций, импорт настроек, регламентное сгорание |
| Клиентский кабинет | /v1/b2c/** — по субъекту клиентского токена |
При превышении лимита ответ 429 несёт заголовок Retry-After и поле
retryAfterSeconds в теле problem+json — вызывающая система знает, когда
повторить попытку.
Проверка загружаемых файлов¶
Любой загружаемый файл (импорт клиентов, справочников, ручных операций и т. п.) проходит проверку до разбора содержимого:
- Сигнатура содержимого — файл обязан быть текстовым: архивы, документы
Office, PDF, изображения и исполняемые файлы отклоняются по первым байтам
независимо от расширения и заявленного
Content-Type. - Строгое декодирование как UTF-8 — «почти UTF-8» (например, файл, сохранённый в Windows-1251) отклоняется явно, а не превращается в испорченные данные.
- Пределы размера (
LOYALTY_IMPORT_MAX_FILE_SIZE, по умолчанию 20 МБ) и числа строк (LOYALTY_IMPORT_MAX_ROWS, по умолчанию 10000 — для импорта клиентов и справочников; у файлового пакета ручных операций свой, больший предел строк, см. «Конфигурация»). - Антивирусная проверка через подключаемый сканер — выключена по умолчанию (сканер у заказчика не определён); при включении работает по принципу fail-closed: недоступность сканера отклоняет файл, а не пропускает его непроверенным.
Отказ на любом из шагов возвращает стабильный код import_*, а не общую
ошибку разбора.
Сверка итогов и контроль целостности¶
POST /v1/admin/reconciliation/runs (роль LOYALTY_MAINTENANCE_WRITE)
запускает внутреннюю сверку: баланс клиентского счёта против суммы проводок
леджера и сумма остатков лотов против баланса счёта; у системных счетов
(источник начислений, списание, сгорание, корректировки, счета партнёров) проекция баланса не ведётся и обязана
оставаться нулевой — ненулевая означает изменение в обход проводок. Это ловит
правку баланса в обход двойной записи — единственный способ, которым учёт может
разъехаться незаметно, — и работает на любом контуре уже сегодня, без внешней
системы.
POST /v1/admin/reconciliation/external-totals (роль
LOYALTY_INTEGRATION_WRITE) принимает итоги операционного дня от внешней
системы (начислено, списано, число операций) и сверяет их с собственными.
Результат каждого прогона сохраняется всегда, включая совпадение без
расхождений; список прогонов и расхождений по прогону читается ролью
LOYALTY_MONITORING_READ (GET /v1/admin/reconciliation/runs,
GET /v1/admin/reconciliation/runs/{runId}/diffs). Расхождения делятся по
серьёзности: ERROR — нарушен инвариант учёта, разбор обязателен; WARN —
объяснимое расхождение (например, демо-данные без проводок).
Недоступность MDM¶
Сбой внешнего реестра клиентов (MDM) — таймаут, обрыв соединения, ошибка
5xx — возвращается как 503 mdm_unavailable, а не как 404 customer_not_found.
Это разные факты: клиента может не быть в реестре, а реестр может быть просто
недоступен, — и вызывающая система обязана различать их, иначе ретраит первое
как второе (повторять 404 бессмысленно, а 503 можно и нужно).
Таблицы смежных систем в карточке клиента¶
Администратор может вывести в карточку клиента данные из таблиц смежных систем, лежащих в той же базе
данных. Чтение ограничено трижды: схемы — только из белого списка LOYALTY_CLIENT_PROFILE_SOURCE_SCHEMAS
(по умолчанию пуст — возможность выключена), таблицы и колонки — только те, на которые у роли приложения
в БД есть права, а имена в запросах берутся из системного каталога БД, а не из запроса пользователя.
Перечень схем, таблиц и колонок доступен только с ролью LOYALTY_DICTIONARY_READ; оператор карточки
видит лишь настроенные столбцы, без имён схемы и таблицы источника.
Состав ПО (SBOM)¶
Каждая сборка образа несёт файл состава ПО (SBOM, формат CycloneDX) со
сведениями о версиях и лицензиях всех зависимостей, включая транзитивные.
Файл едет внутри образа — так он всегда отвечает на вопрос «что именно внутри
вот этого выката» — и позволяет подтвердить состав поставки и отсутствие
запрещённых лицензий при приёмке или разборе инцидента. Для веб-интерфейса перечень зависимостей
в том же формате формирует CI (отчёт GitLab в пайплайнах веток и merge request), внутрь образа
loyalty-ui он не кладётся — см. «Установка и развёртывание» → «Состав ПО».
Подробнее о конкретном релизе — в Release Notes.