Files

206 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-08-21 09:58:08 +03:00
# План доработки: механизм аутентификации 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`.