Модуль iam¶
Статус¶
Созданы каркас Spring Modulith-модуля, публичная граница iam::api, внутренняя модель
UserAccount, отдельные password credentials, членство сотрудника ГК и bootstrap первого
администратора. Реализованы проверка credentials для входа в Админку ГК, обязательная смена
временного пароля, серверные refresh-сессии и access JWT Админки ГК, persistence-модель членства
сотрудника ФЧ, создание и принятие приглашения первого администратора ФЧ и проверка credentials
для входа сотрудника ФЧ, а также его серверные refresh-сессии и access JWT. Учётные записи
разделены по контурам занятости: ГК и ФЧ используют независимые identity.
Назначение¶
Модуль iam отвечает за идентификацию, аутентификацию и авторизацию сотрудников головной компании
и франчайзи.
Модуль будет владеть:
- единой учётной записью сотрудника
UserAccount; - password credentials;
- членствами сотрудников в ГК и ФЧ;
- типизированными ролями и разрешениями;
- приглашениями и одноразовыми setup token;
- refresh-сессиями и отзывом доступа;
- выпуском access token для Админки ГК и Личного кабинета ФЧ;
- операционными блокировками доступа.
Модуль не отвечает за:
- аккаунты и сессии покупателей;
- франчайзи и юридические лица;
- подключения и импорт iiko;
- рестораны;
- HTTP-интерфейсы Админки ГК и Личного кабинета ФЧ.
Покупательский контур принадлежит будущему модулю customer. Совпадение телефона или email не
объединяет аккаунт покупателя и сотрудника.
Границы и зависимости¶
Разрешённая зависимость:
Она используется для реакции на публичное событие FranchiseeStatusChanged: при отключении ФЧ
модуль операционно блокирует его членства и отзывает соответствующие сессии. Собственный статус
членства при этом не изменяется.
Запрещены:
- прямое чтение таблиц
franchise; - зависимость от внутренних пакетов доменных модулей;
- хранение бизнес-данных франчайзи, iiko или ресторанов;
- использование учётной записи сотрудника как аккаунта покупателя.
Будущие модули admin и franchiseoffice будут зависеть от iam::api, но iam не будет зависеть
от них.
Структура пакетов¶
ru.panampizza.monolith.iam
├── api
│ ├── IamApi
│ ├── AdminAuthentication
│ ├── AdminAuthenticationResult
│ ├── AdminAccessTokens
│ ├── AdminAccessToken
│ ├── VerifiedAdminAccess
│ ├── AdminPasswordChange
│ ├── AdminPrincipal
│ ├── AdminSessions
│ ├── AdminSession
│ ├── SessionAudience
│ ├── AdminRole
│ ├── AdminPermission
│ ├── HeadCompanyMembershipStatus
│ ├── FranchiseAuthentication
│ ├── FranchiseAuthenticationResult
│ ├── FranchisePrincipal
│ ├── FranchiseSessions
│ ├── FranchiseSession
│ ├── FranchiseAccessTokens
│ ├── FranchiseAccessToken
│ ├── VerifiedFranchiseAccess
│ ├── FranchiseRole
│ ├── FranchisePermission
│ ├── FranchiseMembershipStatus
│ ├── FranchiseMembershipOperationalBlock
│ └── FranchiseAdminInvitations
└── bootstrap
├── IamBootstrap
├── IamBootstrapResult
├── IamBootstrapConfiguration
└── AdminBootstrapService
Корневой пакет объявлен через @ApplicationModule. Пакет api объявлен через
@NamedInterface("api"). Пакет bootstrap объявлен отдельной технической named interface
iam::bootstrap: её использует только общий процесс первоначальной настройки. Она не предназначена
для обычных application-модулей.
IamApi пока является маркером границы. В iam::api опубликован узкий контракт
AdminAuthentication и FranchiseAuthentication для раздельной проверки доступа сотрудников ГК
и ФЧ. Операции управления аккаунтами остаются внутренними. Публичные операции добавляются вместе
с соответствующими сценариями, а не заранее.
Реализованная модель данных¶
iam.user_account
├── id: UUID
├── context: HEAD_COMPANY | FRANCHISE, immutable
├── email: varchar(320), normalized, unique в паре с context
├── display_name: varchar(200)
├── status: INVITED | ACTIVE | DISABLED
├── created_at
├── updated_at
└── version
iam.password_credential
├── user_id: UUID, PK/FK → user_account
├── password_hash: varchar(255)
├── password_changed_at
├── password_change_required
└── version
iam.head_company_membership
├── user_id: UUID, PK
├── context: HEAD_COMPANY, составной FK (user_id, context) → user_account (id, context)
├── role: ADMIN
├── status: INVITED | ACTIVE | DISABLED
├── created_at
├── updated_at
└── version
iam.login_session
├── id: UUID
├── user_id: UUID, FK → user_account
├── audience: ADMIN | FRANCHISE_OFFICE
├── expires_at
├── created_at
├── revoked_at: nullable
└── version
iam.refresh_credential
├── id: UUID
├── session_id: UUID, FK → login_session
├── generation: integer
├── token_hash: varchar(64), unique
├── created_at
├── rotated_at: nullable
├── successor_id: nullable, FK → refresh_credential
└── version
iam.franchise_membership
├── user_id: UUID, PK
├── context: FRANCHISE, составной FK (user_id, context) → user_account (id, context)
├── franchisee_id: UUID, PK/FK → franchise.franchisee
├── role: ADMIN
├── status: INVITED | ACTIVE | DISABLED
├── operational_block: nullable | FRANCHISEE_DISABLED
├── created_at
├── updated_at
└── version
iam.franchise_invitation
├── id: UUID
├── user_id, franchisee_id: FK → franchise_membership
├── token_hash: varchar(64), unique
├── expires_at
├── created_at
├── revoked_at: nullable
├── accepted_at: nullable
└── version
Password credential отделён от аккаунта, чтобы жизненный цикл identity не зависел от конкретного
способа входа и в будущем можно было добавить SSO без изменения UserAccount.
Email нормализуется как trim + lowercase(Locale.ROOT). Приложение валидирует формат и длину, а
БД проверяет нормализованное значение и уникальность внутри контура, поэтому параллельные операции
не могут создать два аккаунта с одним email в одном контуре.
Контуры занятости¶
UserAccount.context задаётся при создании и неизменяем. Он определяет, к какому контуру относится
учётная запись:
HEAD_COMPANY → сотрудник головной компании, Админка ГК
FRANCHISE → сотрудник франчайзи, Личный кабинет ФЧ
Контуры полностью независимы. Один и тот же email может существовать в обоих: это будут две разные учётные записи с разными идентификаторами, разными password credentials и раздельным жизненным циклом. Совпадение email не объединяет их и не переносит доступ между контурами — тот же принцип, что уже применён к границе покупателей.
Уникальность email действует внутри контура: unique (context, email). Поиск аккаунта всегда
выполняется парой (context, email), поэтому сценарий одного контура не видит и не может изменить
учётные записи другого.
Принадлежность членства контуру закреплена на уровне БД, а не проверками в сервисах. Составной
уникальный ключ user_account (id, context) и составные FK из таблиц членств делают невозможной
привязку членства ГК к учётной записи контура ФЧ и наоборот:
Колонка context в таблицах членств не описывает само членство: её значение всегда константно
(HEAD_COMPANY или FRANCHISE соответственно). Она существует только ради этого внешнего ключа.
Ограничение вида «ссылаться можно лишь на учётную запись своего контура» иначе невыразимо:
константу во внешний ключ подставить нельзя, а подзапрос в check PostgreSQL запрещает. Поэтому
значение фиксируется check-ограничением и участвует в FK вместе с user_id. В JPA колонка не
отображается и заполняется значением по умолчанию, так что в Kotlin-модели членств её нет.
Разделение выбрано потому, что траектории безопасности контуров расходятся: для ГК приоритетны корпоративные SSO и MFA, а внешние партнёры-франчайзи остаются на email и пароле. При общей identity каждая новая возможность аутентификации требовала бы ветвления по типу членства.
Плата за разделение — офбординг человека, работающего в обоих контурах, перестаёт быть атомарной: отключать нужно обе учётные записи. Случай редкий; если он станет значимым, учётные записи одного человека будет группировать отдельный необязательный признак.
Пароль кодируется через DelegatingPasswordEncoder; текущий алгоритм — bcrypt, а сохранённое
значение содержит идентификатор алгоритма {bcrypt}. Открытый пароль не сохраняется. Из-за лимита
bcrypt приложение принимает пароль длиной от 12 до 72 UTF-8 байт и отклоняет более длинное
значение вместо незаметного усечения.
Реализованные внутренние сценарии:
- создать аккаунт в статусе
INVITED; - найти аккаунт по нормализованному email;
- создать или заменить password credential и перевести аккаунт в
ACTIVE.
Аутентификация сотрудника ГК¶
Публичный контракт AdminAuthentication.authenticate(email, password):
- Нормализует email теми же правилами, что и создание аккаунта, и ищет учётную запись парой
(HEAD_COMPANY, email). - Проверяет password credential через
PasswordEncoder. - Требует
UserAccount.status = ACTIVE. - Требует активное
HeadCompanyMembership. - Возвращает типизированный
AdminPrincipal, отображаемое имя и признак обязательной смены пароля.
Неизвестный или некорректный email, неверный пароль, отсутствующее/отключённое членство и
отключённый аккаунт приводят к одной ошибке AdminAuthenticationFailedException, которая не
раскрывает причину отказа.
При password_change_required=true пароль подтверждает личность, но AdminPrincipal возвращается
с пустым набором permissions. Такой principal в будущем сможет использовать только endpoint смены
пароля или завершения сессии. После обычного входа AdminRole.ADMIN преобразуется в полный набор
AdminPermission.
Публичный контракт AdminPasswordChange.changeTemporaryPassword(userId, currentPassword,
newPassword) повторно проверяет текущий пароль, активность аккаунта и членства ГК. Операция
доступна только при password_change_required=true, запрещает повторно использовать текущий пароль
и применяет к новому паролю общие ограничения 12–72 UTF-8 байта. Hash заменяется и временный флаг
снимается атомарно. Результат содержит AdminPrincipal с permissions активной роли.
Отключённый доступ и неверный текущий пароль возвращают общую
AdminAuthenticationFailedException. Невалидный новый пароль, повторное использование пароля и
вызов операции для уже постоянного пароля представлены отдельными ошибками и не изменяют credential.
Членство сотрудника франчайзи¶
FranchiseMembership идентифицируется парой (userId, franchiseeId), а уникальное ограничение на
userId закрепляет железное бизнес-правило: один сотрудник может принадлежать только одному ФЧ.
Система не поддерживает работу одного человека в разных ФЧ. Оба владельца защищены FK: франчайзи
должен существовать в franchise, а учётная запись — в iam и обязательно в контуре FRANCHISE.
Собственный статус и внешняя операционная блокировка хранятся отдельно:
status = ACTIVE, operationalBlock = null → доступ возможен
status = DISABLED, operationalBlock = null → отключено вручную
status = ACTIVE, operationalBlock = FRANCHISEE_DISABLED → заблокировано состоянием ФЧ
Реакция на состояние ФЧ¶
FranchiseeStatusChanged обрабатывается внутренним FranchiseMembershipLifecycleService:
DISABLED → всем членствам ФЧ ставится operationalBlock = FRANCHISEE_DISABLED,
активные сессии аудитории FRANCHISE_OFFICE отзываются
ACTIVE → снимается только блокировка FRANCHISEE_DISABLED
Снятие блокировки не активирует вручную отключённое членство: status и operationalBlock —
независимые поля, и обработчик трогает только второе. Повторное отключение уже заблокированного
членства не меняет updated_at.
Слушатель синхронный и выполняется в транзакции, сменившей статус ФЧ. Это осознанный выбор: блокировка доступа обязана быть атомарной со сменой статуса, иначе между commit и асинхронной обработкой остаётся окно, в котором сотрудник отключённого ФЧ продолжает работать. Ошибка обработчика откатывает и саму смену статуса, поэтому частичного состояния не возникает.
Отзыв сессий выполняется после flush изменённых членств: запрос отзыва помечен
clearAutomatically, который отцепляет entity от persistence context.
Поскольку проверка operationalBlock уже встроена в аутентификацию, выпуск сессий и проверку
access JWT, отключение ФЧ немедленно закрывает все три пути доступа его сотрудников.
На первом этапе опубликована одна роль FranchiseRole.ADMIN, которая получает полный набор
типизированных permissions:
FRANCHISE_VIEW_OWN
LEGAL_ENTITY_MANAGE
EMPLOYEE_INVITE
IIKO_CONNECTION_MANAGE
IIKO_IMPORT_RUN
RESTAURANT_MANAGE
JPA entity и repository остаются внутренними. Публичного метода произвольного создания членства нет: оно будет создаваться только сценарием приглашения.
Приглашение администратора ФЧ¶
Публичный контракт FranchiseAdminInvitations.invite(franchiseeId, email, displayName):
- Проверяет через
franchise::api, что ФЧ существует и находится в статусеACTIVE. - Находит
UserAccountконтураFRANCHISEпо нормализованному email или создаёт его в статусеINVITED. - Создаёт
FranchiseMembership(ADMIN, INVITED), если такого членства ещё нет. - Генерирует 32 случайных байта setup token и возвращает Base64 URL значение техническому вызывающему коду.
- Сохраняет только уникальный SHA-256 hash и срок действия 72 часа.
Поиск на шаге 2 ограничен контуром FRANCHISE. Учётная запись сотрудника ГК с тем же email
сценарию не видна: приглашение создаёт отдельную identity контура ФЧ и не может изменить пароль,
статус или отображаемое имя аккаунта ГК.
Для существующего аккаунта контура ФЧ переданное отображаемое имя не перезаписывает его identity. Отключённый ФЧ, отключённый аккаунт, любое членство в другом ФЧ, активное/отключённое членство и второе незавершённое приглашение отклоняются.
После отзыва или истечения token можно выпустить новое приглашение для того же членства INVITED:
новая строка членства не создаётся. Истёкшее приглашение при повторной выдаче помечается отозванным.
Открытый setup token никогда не сохраняется в БД и не должен возвращаться HTTP-клиенту Админки ГК;
его получит будущий технический адаптер уведомлений.
Публичный контракт FranchiseAdminInvitations.accept(setupToken, password) атомарно:
- Находит и блокирует приглашение по SHA-256 hash токена.
- Проверяет, что приглашение не истекло, не отозвано и ещё не принято, а членство имеет статус
INVITED. - Повторно проверяет актуальность состояния: учётная запись всё ещё
INVITED, а ФЧ всё ещёACTIVE. - Проверяет пароль по общим ограничениям 12–72 UTF-8 байта и сохраняет только его hash.
- Переводит
UserAccountиFranchiseMembershipвACTIVEи отмечает приглашение принятым.
Шаг 3 закрывает окно между выдачей и погашением token: за отведённые 72 часа учётную запись могли
отключить, а ФЧ — деактивировать. Приглашение не реактивирует отключённый аккаунт и не открывает
доступ к отключённому ФЧ. Установка пароля возможна только для учётной записи в статусе INVITED,
поэтому сценарий приглашения не может заменить credential уже действующего сотрудника.
Неизвестный, истёкший, отозванный и уже использованный токен, отключённый аккаунт и отключённый ФЧ
дают одинаковую FranchiseInvitationAcceptanceFailedException. Setup token одноразовый. Ошибка на
любом этапе откатывает всю транзакцию, поэтому аккаунт, credential, членство и приглашение не могут
оказаться в частично активированном состоянии.
revoke(invitationId) отзывает незавершённое приглашение. Принятое приглашение в будущем нельзя
будет отозвать этим сценарием.
Аутентификация сотрудника ФЧ¶
Публичный контракт FranchiseAuthentication.authenticate(email, password):
- Нормализует email, ищет учётную запись парой
(FRANCHISE, email)и проверяет её password credential. - Требует
UserAccount.status = ACTIVE. - Требует единственное
FranchiseMembershipв статусеACTIVEбезoperationalBlock. - Возвращает отдельный
FranchisePrincipalсuserId,franchiseeIdи типизированными permissions роли.
Неизвестный или некорректный email, неверный пароль, отсутствующий credential или членство,
неактивный аккаунт, неактивное либо операционно заблокированное членство дают одну
FranchiseAuthenticationFailedException, не раскрывающую причину отказа.
Если учётная запись требует смены временного пароля, credentials подтверждаются, но
FranchisePrincipal получает пустой набор permissions. Принятие приглашения сотрудника ФЧ сразу
устанавливает постоянный пароль, поэтому в текущих сценариях этот режим не возникает; он сохранён
для симметрии с контуром ГК и будущей выдачи временных паролей сотрудникам ФЧ.
Refresh-сессии Админки ГК¶
Публичный контракт AdminSessions предоставляет выпуск, ротацию, отзыв одного token и отзыв всех
сессий пользователя. Каждому входу на устройстве соответствует стабильная iam.login_session, а
поколения refresh token хранятся в iam.refresh_credential. В БД сохраняется только уникальный
SHA-256 hash token; открытое значение воспроизводится по ID credential через HMAC с секретом
аудитории. Текущий абсолютный срок login-session — 30 дней.
Сессия имеет аудиторию ADMIN и создаётся только для активного аккаунта с активным членством ГК.
Временный пароль допускает ограниченную сессию, principal которой не содержит permissions: она
предназначена для будущего HTTP endpoint обязательной смены пароля.
Ротация блокирует credential в PostgreSQL, создаёт следующее поколение и сохраняет прежний ID
login-session как sid. Повтор старого token в течение 30 секунд возвращает тот же successor и
является идемпотентным: это устраняет гонку общей cookie между вкладками. Повтор после окна
считается reuse, отзывает всю login-session и возвращает AdminSessionRejectedException.
Истёкшая или отозванная login-session, отключённый аккаунт или членство также запрещают ротацию.
revokeAll(userId) отзывает только активные сессии аудитории ADMIN. Успешная смена временного
пароля внутренней операцией отзывает все сессии учётной записи в той же транзакции, поэтому ранее
выданные ограниченные сессии больше не могут быть использованы. Так как учётная запись принадлежит
ровно одному контуру, сессии другого контура при этом не затрагиваются.
Refresh-сессии Личного кабинета ФЧ¶
Отдельный публичный контракт FranchiseSessions предоставляет выпуск, идемпотентную ротацию,
отзыв текущей login-session и отзыв всех сессий сотрудника аудитории FRANCHISE_OFFICE. Модель
login-session, поколений credentials, HMAC и 30-секундного окна совпадает с admin-контуром, но
публичные Kotlin-типы не пересекаются.
Создание и ротация требуют активного аккаунта, password credential и активного
FranchiseMembership без operationalBlock. Результат содержит FranchisePrincipal с
franchiseeId единственного членства. При временном пароле permissions остаются пустыми.
Ротация сохраняет стабильный sid; reuse за пределами окна отзывает только соответствующую
login-session устройства. Неизвестный, истёкший, отозванный token и token другой аудитории дают
общую FranchiseSessionRejectedException. Операции
revoke и revokeAll ограничены аудиторией FRANCHISE_OFFICE; симметрично AdminSessions теперь
отзывает только аудиторию ADMIN. Поэтому API одного контура не может погасить сессию другого.
Access JWT Админки ГК¶
Публичный контракт AdminAccessTokens выпускает token для существующей серверной сессии и
проверяет полученный token. JWT подписывается HS256 и действует 15 минут, но не дольше самой
refresh-сессии.
Обязательные claims:
sub user ID
sid login-session ID
jti уникальный ID access token
iss настраиваемый issuer, по умолчанию panam
aud admin
typ access
membership HEAD_COMPANY
permissions AdminPermission[]
iat, nbf, exp временные ограничения
JOSE header содержит alg=HS256 и typ=at+jwt. Проверка требует точного совпадения алгоритма,
типа, issuer, единственной audience, membership и временных claims. После криптографической
проверки IAM загружает серверную сессию по sid и повторно проверяет её аудиторию, срок и отзыв, а
также текущую активность аккаунта и членства ГК. Поэтому отзыв refresh-сессии немедленно делает
связанный access token недействительным. Набор permissions в JWT должен точно совпадать с текущим
набором доступа.
При временном пароле выпускается подписанный token с пустыми permissions. Он подтверждает личность для будущего endpoint смены пароля, но не разрешает административные операции.
Настройки имеют префикс panam.iam.admin-jwt:
PANAM_IAM_ADMIN_JWT_ISSUER по умолчанию panam
PANAM_IAM_ADMIN_JWT_ACCESS_TTL по умолчанию 15m
PANAM_IAM_ADMIN_JWT_SECRET
PANAM_IAM_ADMIN_JWT_SECRET_FILE
Должен быть указан ровно один источник секрета: прямое значение или файл. Секрет должен содержать
не менее 32 UTF-8 байт. Настройка проверяется при старте приложения: неверная или отсутствующая
конфигурация не даёт подняться контексту, вместо того чтобы превратиться в 500 при первом входе.
Процессы, которые не создают bean выпуска access token (включая bootstrap CLI), JWT secret
по-прежнему не требуют. Для Kubernetes и общей тестовой среды предпочтителен файл, смонтированный
из Secret.
Access JWT Личного кабинета ФЧ¶
Публичный контракт FranchiseAccessTokens использует отдельные типы token и результата проверки,
а также отдельные настройки и секрет. JWT подписывается HS256, действует 15 минут, но не дольше
связанной refresh-сессии FRANCHISE_OFFICE.
Обязательные claims:
sub user ID
sid login-session ID
jti уникальный ID access token
iss настраиваемый issuer, по умолчанию panam
aud franchise-office
typ access
membership FRANCHISE
franchisee_id ID единственного ФЧ сотрудника
permissions FranchisePermission[]
iat, nbf, exp временные ограничения
При проверке IAM требует HS256, JOSE typ=at+jwt, точные audience, membership и временные claims,
после чего повторно загружает серверную сессию, аккаунт, credential и членство. sub,
franchisee_id и permissions должны совпадать с текущими данными. Отозванная или истёкшая сессия,
неактивный аккаунт либо членство и любой operationalBlock немедленно делают access token
недействительным. Admin JWT не принимается даже при одинаковом значении signing secret.
Настройки имеют префикс panam.iam.franchise-jwt:
PANAM_IAM_FRANCHISE_JWT_ISSUER по умолчанию panam
PANAM_IAM_FRANCHISE_JWT_ACCESS_TTL по умолчанию 15m
PANAM_IAM_FRANCHISE_JWT_SECRET
PANAM_IAM_FRANCHISE_JWT_SECRET_FILE
Должен быть указан ровно один источник секрета, содержащий не менее 32 UTF-8 байт. Для Kubernetes поддерживается чтение из Secret volume; завершающий перевод строки файла удаляется.
В iam::api опубликованы отдельные типы AdminRole, AdminPermission и
HeadCompanyMembershipStatus. Роль AdminRole.ADMIN отображается на полный набор типизированных
административных permissions. Строковые роли общего назначения не используются.
Bootstrap первого администратора¶
Flyway создаёт только структуру БД и не содержит известного логина или пароля. При явно включённом bootstrap приложение атомарно создаёт:
UserAccount(HEAD_COMPANY, ACTIVE)
PasswordCredential(password_change_required=true)
HeadCompanyMembership(ADMIN, ACTIVE)
Признаком уже выполненного bootstrap служит наличие любого HeadCompanyMembership, в том числе
отключённого. Повторный запуск ничего не создаёт, не меняет пароль и не добавляет членства.
Bootstrap запускается одним отдельным процессом; одновременный запуск нескольких bootstrap-процессов
не поддерживается, поскольку эта операция выполняется один раз за жизнь установки.
Временный пароль нельзя использовать для административных операций до его замены: сценарий входа возвращает principal без permissions. Смена временного пароля снимает флаг и отзывает все выданные до неё refresh-сессии в той же транзакции.
Настройки читает общий bootstrap-процесс. Имена следуют правилу
$PROJECT_BOOTSTRAP_$MODULE_$ENTITY_$PROPERTY:
PANAM_BOOTSTRAP_IAM_ADMIN_EMAIL
PANAM_BOOTSTRAP_IAM_ADMIN_DISPLAY_NAME
PANAM_BOOTSTRAP_IAM_ADMIN_PASSWORD
PANAM_BOOTSTRAP_IAM_ADMIN_PASSWORD_FILE
Должен быть указан ровно один источник пароля: прямое значение или путь к файлу. Файл предпочтителен для общих сред. Значения секрета не выводятся в лог.
Локальный запуск¶
После task env:up выполняется:
Скрипт интерактивно читает email, имя и пароль, временно сохраняет пароль в файл с правами 0600,
вызывает общую Gradle-команду ./gradlew bootstrap и удаляет файл при завершении.
Docker Compose¶
Когда приложение будет добавлено в Compose тестовой среды, bootstrap оформляется отдельным одноразовым service/profile, а пароль передаётся стандартным secret:
services:
iam-bootstrap:
profiles: [bootstrap]
command: [bootstrap]
environment:
PANAM_BOOTSTRAP_IAM_ADMIN_EMAIL: admin@test.example
PANAM_BOOTSTRAP_IAM_ADMIN_DISPLAY_NAME: Test Admin
PANAM_BOOTSTRAP_IAM_ADMIN_PASSWORD_FILE: /run/secrets/bootstrap_admin_password
secrets:
- bootstrap_admin_password
restart: "no"
secrets:
bootstrap_admin_password:
file: .secrets/bootstrap-admin-password
Текущий локальный compose.yaml намеренно остаётся минимальным и содержит только PostgreSQL.
Kubernetes¶
Bootstrap запускается однократным Kubernetes Job. Secret монтируется файлом, а Job передаёт его
путь через PANAM_BOOTSTRAP_IAM_ADMIN_PASSWORD_FILE. Доступ в shell контейнера не требуется.
Основной Deployment запускается без bootstrap-настроек. После обязательной смены пароля Secret
следует удалить.
Устройство общего изолированного контекста и правила подключения следующих модулей описаны в архитектуре bootstrap-процесса.
Контуры доступа¶
iam обслуживает два независимых административных контура, каждый со своими учётными записями:
UserAccount(HEAD_COMPANY) → AdminPrincipal → aud=admin → Админка ГК
UserAccount(FRANCHISE) → FranchisePrincipal → aud=franchise-office → Личный кабинет ФЧ
Principals, роли и permissions будут разными Kotlin-типами. Общий principal для прикладных операций не вводится.
Покупатель использует отдельные CustomerAccount, CustomerPrincipal и aud=storefront, которые
не входят в этот модуль.
Проверка модульности¶
Общий ModularityTests вызывает ApplicationModules.verify() и проверяет разрешённые зависимости,
обращения к named interfaces и отсутствие циклов.
Следующий этап¶
Минимальный auth-контур модуля admin, FranchiseMembership, приглашения, внутренняя
аутентификация, серверные refresh-сессии, access JWT и полный HTTP auth-контур сотрудника ФЧ
реализованы. Следующий отдельно принимаемый этап относится к прикладным сценариям Личного кабинета.
Полная модель сотрудников описана в модели административного доступа, а граница покупателей — в модели доступа покупателей.