Files
cloud-ip-validator/docs/PLAN_OPENSTACK_AUTH.md
T
2026-08-21 09:58:08 +03:00

207 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План доработки: механизм аутентификации 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`.