147 lines
3.7 KiB
Go
147 lines
3.7 KiB
Go
package secrets
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
infisical "github.com/infisical/go-sdk"
|
|
)
|
|
|
|
// InfisicalBackend reads secrets from an Infisical instance via machine identity auth.
|
|
type InfisicalBackend struct {
|
|
client infisical.InfisicalClientInterface
|
|
cfg InfisicalConfig
|
|
mu sync.Mutex
|
|
connected bool
|
|
}
|
|
|
|
// InfisicalConfig holds connection parameters for an Infisical instance.
|
|
type InfisicalConfig struct {
|
|
SiteURL string // Infisical server URL (cloud or self-hosted)
|
|
ClientID string // Machine identity client ID
|
|
ClientSecret string // Machine identity client secret
|
|
ProjectID string // Infisical project/workspace ID
|
|
SecretPath string // Path prefix for secrets (e.g., "/")
|
|
Env string // Environment slug (e.g., "dev", "prod")
|
|
}
|
|
|
|
// NewInfisicalBackend creates an Infisical backend. Connects lazily on first Get.
|
|
func NewInfisicalBackend(cfg InfisicalConfig) *InfisicalBackend {
|
|
autoRefresh := true
|
|
|
|
return &InfisicalBackend{
|
|
cfg: cfg,
|
|
client: infisical.NewInfisicalClient(context.Background(), infisical.Config{
|
|
SiteUrl: cfg.SiteURL,
|
|
AutoTokenRefresh: &autoRefresh,
|
|
CacheExpiryInSeconds: 0, // no caching — live reads over localhost
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (b *InfisicalBackend) Name() string { return "infisical" }
|
|
|
|
func (b *InfisicalBackend) connect() error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
if b.connected {
|
|
return nil
|
|
}
|
|
|
|
clientID := b.cfg.ClientID
|
|
clientSecret := b.cfg.ClientSecret
|
|
if clientID == "" || clientSecret == "" {
|
|
return fmt.Errorf("%w: OIKOS_INFISICAL_CLIENT_ID and OIKOS_INFISICAL_CLIENT_SECRET not set", ErrBackendUnavailable)
|
|
}
|
|
|
|
_, err := b.client.Auth().UniversalAuthLogin(clientID, clientSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: infisical auth: %w", ErrBackendUnavailable, err)
|
|
}
|
|
|
|
b.connected = true
|
|
return nil
|
|
}
|
|
|
|
func (b *InfisicalBackend) Get(ctx context.Context, key string) (string, error) {
|
|
if err := b.connect(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
opts := infisical.ListSecretsOptions{
|
|
ProjectID: b.cfg.ProjectID,
|
|
Environment: b.cfg.Env,
|
|
SecretPath: b.cfg.SecretPath,
|
|
}
|
|
|
|
secrets, err := b.client.Secrets().List(opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("infisical list: %w", err)
|
|
}
|
|
|
|
// Strip path prefix for matching
|
|
lookupKey := strings.TrimPrefix(key, b.cfg.SecretPath)
|
|
|
|
for _, s := range secrets {
|
|
if s.SecretKey == lookupKey {
|
|
return s.SecretValue, nil
|
|
}
|
|
}
|
|
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
func (b *InfisicalBackend) List(ctx context.Context) ([]string, error) {
|
|
if err := b.connect(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts := infisical.ListSecretsOptions{
|
|
ProjectID: b.cfg.ProjectID,
|
|
Environment: b.cfg.Env,
|
|
SecretPath: b.cfg.SecretPath,
|
|
}
|
|
|
|
secrets, err := b.client.Secrets().List(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("infisical list: %w", err)
|
|
}
|
|
|
|
keys := make([]string, len(secrets))
|
|
for i, s := range secrets {
|
|
keys[i] = s.SecretKey
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
func (b *InfisicalBackend) Set(ctx context.Context, key string, value string) error {
|
|
if err := b.connect(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Try create first (idempotent — upserts); fall back to update on conflict.
|
|
_, err := b.client.Secrets().Create(infisical.CreateSecretOptions{
|
|
SecretKey: key,
|
|
SecretValue: value,
|
|
Environment: b.cfg.Env,
|
|
SecretPath: b.cfg.SecretPath,
|
|
ProjectID: b.cfg.ProjectID,
|
|
Type: "shared",
|
|
})
|
|
if err != nil {
|
|
// Create failed (key may already exist) — update instead.
|
|
_, err = b.client.Secrets().Update(infisical.UpdateSecretOptions{
|
|
SecretKey: key,
|
|
NewSecretValue: value,
|
|
Environment: b.cfg.Env,
|
|
SecretPath: b.cfg.SecretPath,
|
|
ProjectID: b.cfg.ProjectID,
|
|
Type: "shared",
|
|
})
|
|
}
|
|
return err
|
|
}
|