13 KiB
План доработки: механизм аутентификации OpenStack-клиента
Статус: реализуется (см. коммиты после этого документа). Документ фиксирует согласованный дизайн доработки
internal/openstack— поддержка двух режимов аутентификации (token passthrough / password auto-obtain) и приведение имён переменных окружения к реальному набору пользователя.
Context
В ходе разбора выяснилось, что переменные окружения OpenStack нужны не только «токену действовать», а именно чтобы получить токен — пользователь предоставляет стандартный набор credentials, принятый в python-openstackclient/RC-файлах:
OS_AUTH_URL=https://infra.mail.ru:35357/v3/
OS_PROJECT_ID=366fdd4249984226a023f13ac94e226e
OS_REGION_NAME=RegionOne
OS_USERNAME=a.yurishchev
OS_USER_DOMAIN_NAME=users
OS_PASSWORD=<секрет>
OS_INTERFACE=public
OS_IDENTITY_API_VERSION=3
Текущий код (internal/openstack/client.go) поддерживает только один
режим: обменять переданный OS_TOKEN на новый scoped-токен через
POST /auth/tokens (identity method "token"), потому что всегда
выставляет authOpts.Scope. Разбор исходников gophercloud показал, что
библиотека уже умеет и второй, более простой режим: если Scope не
задан, а TokenID задан — включается «passthrough»-путь (v3auth в
openstack/client.go пакета gophercloud): токен валидируется через
GET /v3/auth/tokens (X-Subject-Token: <тот же токен>) и используется
как есть для всех последующих вызовов Neutron — новый токен не
выпускается. Это ровно то поведение, которое просит пользователь:
«для выполнения API-вызовов следует использовать токен, который передал
администратор», без скрытого обмена.
Итоговое требование: два режима аутентификации, выбираемые конфигурацией:
token(по умолчанию) — админ передаёт уже готовый, заранее scoped на нужный проект токен черезOS_TOKEN; код использует его как есть (passthrough), никогда не переобменивает. Автообновление здесь невозможно в принципе (нечем) — если токен истёк,control-apiупадёт с ошибкой аутентификации, токен нужно перевыпустить и обновить env вручную (это принимается как компромисс данного режима, не баг).password(опционально) — админ передаётOS_USERNAME/OS_PASSWORD/OS_USER_DOMAIN_NAME; код сам получает токен через обычную password-аутентификацию Keystone v3, сAllowReauth: true— токен самообновляется автоматически при 401 в течение всего времени жизни процесса (не ограничен TTL одного токена, в отличие от режимаtoken).
Набор имён переменных окружения также приводится в соответствие с
реальным набором пользователя (стандартные OS_*-имена
python-openstackclient), включая ранее не читавшиеся код OS_USERNAME,
OS_USER_DOMAIN_NAME, OS_PASSWORD, OS_INTERFACE. OS_PROJECT_NAME и
OS_PROJECT_DOMAIN_NAME из текущего кода убираются как неиспользуемые —
у пользователя их нет, и они не нужны: в Keystone v3 scope по одному
OS_PROJECT_ID достаточен, доменной привязки проекта не требует.
OS_IDENTITY_API_VERSION не потребляется кодом — он и так всегда ходит в
Keystone v3 (AuthenticateV3/v3auth), читать/валидировать эту
переменную незачем.
Дизайн
1. internal/openstack/client.go — новый ClientConfig и NewClient
type AuthMethod string
const (
AuthMethodToken AuthMethod = "token" // passthrough, по умолчанию
AuthMethodPassword AuthMethod = "password" // авто-получение, опционально
)
type ClientConfig struct {
AuthURL string
Method AuthMethod // "" трактуется как AuthMethodToken
Token string // для Method == token
Username string // для Method == password
Password string
UserDomainName string
ProjectID string
Region string
Interface string // "public" | "internal" | "admin" — совпадает по значениям с gophercloud.Availability, маппинг не нужен
}
Вынести сборку gophercloud.AuthOptions в отдельную чистую функцию
buildAuthOptions(cfg ClientConfig) (gophercloud.AuthOptions, error)
(без сети) — специально, чтобы её можно было покрыть юнит-тестами без
реального OpenStack:
Method == AuthMethodPassword: требуетUsername+Password(иначе ошибка);Username,Password,DomainName: cfg.UserDomainName,Scope: &gophercloud.AuthScope{ProjectID: cfg.ProjectID},AllowReauth: true.Method == AuthMethodToken(или"", по умолчанию): требуетToken(иначе ошибка);TokenID: cfg.Token.Scopeнамеренно не выставляется — это и есть переключатель на passthrough-путь вgophercloud.AllowReauthне выставляется (gophercloudсам вернёт ошибку, если включитьAllowReauthбезScope— см. явную проверку вv3auth), с комментарием почему.- Неизвестный
Method— явная ошибка на старте, а не тихий фоллбэк.
NewClient дальше: AuthenticatedClient(ctx, authOpts) (без изменений в
логике вызова) → резолвинг networking-клиента с
gophercloud.EndpointOpts{Region: cfg.Region, Availability: gophercloud.Availability(cfg.Interface)}
(пустая строка Interface → явно дефолтить "public").
2. internal/config/config.go — OpenStackConfig
Обновить набор *_env-полей до реального использования (индирекция «имя переменной → значение» сохраняется, но список и дефолты приводятся в соответствие):
type OpenStackConfig struct {
Mode string `yaml:"mode"` // "mock" | "real"
AuthMethod string `yaml:"auth_method"` // "token" (default) | "password"
AuthURLEnv string `yaml:"auth_url_env"` // default OS_AUTH_URL
ProjectIDEnv string `yaml:"project_id_env"` // default OS_PROJECT_ID
RegionEnv string `yaml:"region_env"` // default OS_REGION_NAME
InterfaceEnv string `yaml:"interface_env"` // default OS_INTERFACE
TokenEnv string `yaml:"token_env"` // default OS_TOKEN — auth_method: token
UsernameEnv string `yaml:"username_env"` // default OS_USERNAME — auth_method: password
UserDomainNameEnv string `yaml:"user_domain_name_env"` // default OS_USER_DOMAIN_NAME
PasswordEnv string `yaml:"password_env"` // default OS_PASSWORD
}
Убрать ProjectNameEnv/ProjectDomainEnv (не используются в реальном
наборе, Keystone v3 scope по ID их не требует — упрощение, а не потеря
функциональности).
3. cmd/control-api/main.go — newOpenStackClient
- Читает
cfg.OpenStack.AuthMethod(""/"token"→openstack.AuthMethodToken,"password"→openstack.AuthMethodPassword, иначе — отказ старта). - Для
token: требует непустойos.Getenv(TokenEnv)— иначе явная ошибка старта с именем недостающей переменной. - Для
password: требует непустыеUsername/Password— иначе явная ошибка старта. AuthURL/ProjectID/Region— обязательны в обоих режимах, как и сейчас.Interface— необязателен (пустая строка =publicпо умолчанию, см. выше).
4. Конфиг и документация
configs/control-api.example.yaml: секцияopenstackпереписывается под новый набор полей и под пример из реального использования (два примера в комментариях —auth_method: tokenиauth_method: password).deploy/systemd/control-api.service/docs/SETUP.md: обновить примерcontrol-api.envпод оба режима, объяснить компромисс режимаtoken(нет автообновления, нужен ручной перевыпуск при истечении) vspassword(самообновляется, но требует хранить пароль в env-файле).docs/API.md/docs/DIAGRAMS.mdне затрагиваются — это внутренний механизм клиента, наружу в HTTP API не выходит.
5. Тесты
- Новый
internal/openstack/client_test.go: юнит-тесты чистой функцииbuildAuthOptions— без сети, безOPENSTACK_LIVE_TEST:token:Scope == nil,TokenIDравен переданному,AllowReauth == false.tokenбезToken→ ошибка.password:Scope != nilиScope.ProjectIDверный,AllowReauth == true,Username/Password/DomainNameпрокинуты.passwordбезUsername/Password→ ошибка.- неизвестный
Method→ ошибка.
internal/openstack/client_live_test.go(уже существует, гейтитсяOPENSTACK_LIVE_TEST=1): расширить, чтобы читалOS_AUTH_METHOD(token/password) и гонялGetFloatingIPByAddressв обоих режимах, если для них заданы переменные — ручная проверка соответствия дизайна реальному Keystone (Mail.ru/VK Cloud), не часть обычногоgo test ./....
Критичные файлы
internal/openstack/client.go(основная переработка)internal/openstack/client_test.go(новый)internal/openstack/client_live_test.go(расширение)internal/config/config.go(OpenStackConfig)cmd/control-api/main.go(newOpenStackClient)configs/control-api.example.yaml,docs/SETUP.md,deploy/systemd/control-api.service
Проверка
go build ./... && go test ./...— новые юнит-тестыbuildAuthOptionsпроходят офлайн вместе со всем остальным.scripts/run-local-e2e.shне затрагивается (используетopenstack.mode: mock, минуетNewClientцеликом) — должен по-прежнему проходить без изменений в поведении.- Ручная проверка с реальными данными пользователя (вне автоматических
тестов, наружу секреты не публикуются): поднять
control-apiсopenstack.mode: real,auth_method: token, реальнымOS_TOKEN— убедиться, чтоGetFloatingIPByAddress/associate/disassociate проходят без 401; затем повторить сauth_method: passwordиOS_USERNAME/OS_PASSWORD/OS_USER_DOMAIN_NAME— сверить, что оба режима реально авторизуются вhttps://infra.mail.ru:35357/v3/и работают с проектом366fdd4249984226a023f13ac94e226eвRegionOne.