# План доработки: механизм аутентификации 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-вызовов следует использовать токен, который передал администратор», без скрытого обмена. Итоговое требование: **два режима аутентификации**, выбираемые конфигурацией: 1. **`token` (по умолчанию)** — админ передаёт уже готовый, заранее scoped на нужный проект токен через `OS_TOKEN`; код использует его как есть (passthrough), никогда не переобменивает. Автообновление здесь невозможно в принципе (нечем) — если токен истёк, `control-api` упадёт с ошибкой аутентификации, токен нужно перевыпустить и обновить env вручную (это принимается как компромисс данного режима, не баг). 2. **`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` ```go 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-полей до реального использования (индирекция «имя переменной → значение» сохраняется, но список и дефолты приводятся в соответствие): ```go 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` (нет автообновления, нужен ручной перевыпуск при истечении) vs `password` (самообновляется, но требует хранить пароль в 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` ## Проверка 1. `go build ./... && go test ./...` — новые юнит-тесты `buildAuthOptions` проходят офлайн вместе со всем остальным. 2. `scripts/run-local-e2e.sh` не затрагивается (использует `openstack.mode: mock`, минует `NewClient` целиком) — должен по-прежнему проходить без изменений в поведении. 3. Ручная проверка с реальными данными пользователя (вне автоматических тестов, наружу секреты не публикуются): поднять `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`.