207 lines
13 KiB
Markdown
207 lines
13 KiB
Markdown
# План доработки: механизм аутентификации 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`.
|