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

13 KiB
Raw Blame History

План доработки: механизм аутентификации 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

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-полей до реального использования (индирекция «имя переменной → значение» сохраняется, но список и дефолты приводятся в соответствие):

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.