feat(selectel): project-scoped IAM auth with token cache; provider Validate

Selectel Cloud DNS v2 requires a project IAM token in X-Auth-Token, not the
raw service-user secret; the previous client sent the static secret directly
and got 401. The client now parses Credentials.Secret as a Creds JSON blob
(username/password/account_id/project_name), exchanges it for a token via
the Identity API (POST /identity/v3/auth/tokens), and caches the token in
memory per-account until 5 minutes before expiry. ListZones/GetRecords/
ApplyChanges send the cached IAM token instead of the raw secret.

provider.Provider gains a Validate(ctx, Credentials) method so a bad account
can be rejected via trial login at creation time; all Provider fakes across
provider/registry/api/service test packages implement it as a no-op stub for
now (Task 2 will make api's mock configurable).

Security: the service-user password is folded into the token cache key via
SHA-256 (never stored in the clear) so a password change invalidates the
cached token; identity errors are generic and never echo the request body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-04 20:02:36 +07:00
parent 617b02dbfb
commit 32107571d1
7 changed files with 403 additions and 58 deletions
+6 -2
View File
@@ -7,8 +7,9 @@ import (
"github.com/vasyakrg/dns-autoresolver/internal/model"
)
// Credentials holds the secret used to authenticate against a provider.
// For Selectel this is the project-scoped token sent as X-Auth-Token.
// Credentials holds the provider-specific secret. It is stored encrypted and,
// once decrypted, is a provider-defined value — for Selectel a JSON blob with
// the service-user credentials (see selectel.Creds).
type Credentials struct {
Secret string
}
@@ -25,4 +26,7 @@ type Provider interface {
ListZones(ctx context.Context, creds Credentials) ([]Zone, error)
GetRecords(ctx context.Context, creds Credentials, zoneID string) ([]model.Record, error)
ApplyChanges(ctx context.Context, creds Credentials, zoneID string, cs diff.Changeset) error
// Validate checks the credentials are usable (e.g. a trial auth), so a
// bad account is rejected at creation time rather than at first import.
Validate(ctx context.Context, creds Credentials) error
}