mirror of
https://github.com/hanzoai/kms-operator.git
synced 2026-10-30 18:32:57 +00:00
The six kinds (KMSSecret, KMSDynamicSecret, KMSPushSecret, ClusterGenerator, Password, UUID) move from v1alpha1 to v1. One package of Go types, renamed api/v1alpha1 -> api/v1, so there is one definition of each kind rather than a copy per version. The CRDs declare conversion: None, so the API server relabels apiVersion and returns the same bytes. The operator therefore needs to watch only the version the CRD serves; a second Go package would add drift and buy nothing. The v1 schema is v1alpha1's, unchanged -- controller-gen's output moves by exactly one line per CRD, the version name. Call sites import the package as secretsv1 because packages/controllerhelpers already binds bare v1 to k8s.io/api/apps/v1. RBAC is group- and resource-scoped, so it is unaffected.
97 lines
3.4 KiB
Go
97 lines
3.4 KiB
Go
// Package util/secrets.go: read-side helpers that turn a successful
|
|
// auth + scope into a slice of plaintext key/value pairs.
|
|
//
|
|
// The canonical luxfi/kms surface has no list endpoint — every value
|
|
// MUST be addressed by exact name. The caller therefore has to
|
|
// enumerate the expected keys on the CR via secretsScope.keys, and
|
|
// this helper fans out one GET per name. NotFound is treated as
|
|
// fatal: empty-fetch fail-closed, the controller refuses to project
|
|
// an empty Secret.
|
|
package util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
secretsv1 "github.com/hanzoai/kms-operator/api/v1"
|
|
"github.com/hanzoai/kms-operator/packages/kmsapi"
|
|
"github.com/hanzoai/kms-operator/packages/model"
|
|
)
|
|
|
|
// GetPlainTextSecretsViaMachineIdentity fetches every key listed in
|
|
// secretsScope.keys from the canonical /v1/kms/orgs/{org}/secrets/...
|
|
// surface and returns them as a flat slice for the projection layer.
|
|
//
|
|
// Validation:
|
|
// - secretsScope.keys MUST be non-empty (no list endpoint exists).
|
|
// - Per-input bounds enforced via kmsapi.validateScope.
|
|
// - Each value is scanned for control bytes; NUL or C0 fails the
|
|
// entire reconcile rather than truncate downstream.
|
|
func GetPlainTextSecretsViaMachineIdentity(
|
|
ctx context.Context,
|
|
kmsClient kmsapi.Transport,
|
|
host string,
|
|
bearerToken string,
|
|
scope secretsv1.MachineIdentityScopeInWorkspace,
|
|
) ([]model.SingleEnvironmentVariable, error) {
|
|
|
|
if kmsClient == nil {
|
|
return nil, errors.New("GetPlainTextSecretsViaMachineIdentity: kms client is nil")
|
|
}
|
|
if bearerToken == "" {
|
|
return nil, errors.New("GetPlainTextSecretsViaMachineIdentity: empty bearer token")
|
|
}
|
|
if scope.ProjectSlug == "" {
|
|
return nil, errors.New("secretsScope.projectSlug is required")
|
|
}
|
|
if scope.EnvSlug == "" {
|
|
return nil, errors.New("secretsScope.envSlug is required")
|
|
}
|
|
if len(scope.Keys) == 0 {
|
|
return nil, errors.New(
|
|
"secretsScope.keys is required (luxfi/kms has no list endpoint — enumerate explicitly)",
|
|
)
|
|
}
|
|
if len(scope.Keys) > kmsapi.MaxKeysPerCR {
|
|
return nil, fmt.Errorf("secretsScope.keys exceeds %d entries", kmsapi.MaxKeysPerCR)
|
|
}
|
|
|
|
scopePath := kmsapi.NormaliseScopePath(scope.SecretsPath)
|
|
|
|
out := make([]model.SingleEnvironmentVariable, 0, len(scope.Keys))
|
|
for _, key := range scope.Keys {
|
|
if key == "" {
|
|
return nil, errors.New("secretsScope.keys contains an empty entry")
|
|
}
|
|
resp, err := kmsClient.GetSecret(ctx, host, bearerToken, scope.ProjectSlug, scope.EnvSlug, scopePath, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch secret %q: %w", key, err)
|
|
}
|
|
// Binary convention: KMS values are strings and the transport
|
|
// rejects control bytes, so raw binary material (e.g. a BLS
|
|
// signer key) is stored base64-encoded under a name ending in
|
|
// ".b64". On projection the suffix is stripped and the value
|
|
// decoded, so the managed Secret keeps the exact byte contract
|
|
// its consumers expect. One transform, one place.
|
|
name, value := key, resp.Value
|
|
if stripped, ok := strings.CutSuffix(key, ".b64"); ok {
|
|
raw, decErr := base64.StdEncoding.DecodeString(strings.TrimSpace(resp.Value))
|
|
if decErr != nil {
|
|
return nil, fmt.Errorf("secret %q: value is not valid base64: %w", key, decErr)
|
|
}
|
|
name, value = stripped, string(raw)
|
|
}
|
|
out = append(out, model.SingleEnvironmentVariable{
|
|
Key: name,
|
|
Value: value,
|
|
Type: "shared",
|
|
ID: "", // canonical surface has no separate ID
|
|
SecretPath: scope.SecretsPath,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|