206 lines
13 KiB
Markdown
206 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`.
|