967 lines
24 KiB
Go
967 lines
24 KiB
Go
package secrets
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
|
)
|
|
|
|
// Test: GenerateSecret generates valid secrets
|
|
func TestGenerateSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
length int
|
|
want int
|
|
}{
|
|
{
|
|
name: "default length",
|
|
length: DefaultSecretLength,
|
|
want: DefaultSecretLength,
|
|
},
|
|
{
|
|
name: "custom length 64",
|
|
length: 64,
|
|
want: 64,
|
|
},
|
|
{
|
|
name: "custom length 128",
|
|
length: 128,
|
|
want: 128,
|
|
},
|
|
{
|
|
name: "zero length defaults to DefaultSecretLength",
|
|
length: 0,
|
|
want: DefaultSecretLength,
|
|
},
|
|
{
|
|
name: "negative length defaults to DefaultSecretLength",
|
|
length: -1,
|
|
want: DefaultSecretLength,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
secret, err := GenerateSecret(tt.length)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSecret failed: %v", err)
|
|
}
|
|
|
|
if len(secret) != tt.want {
|
|
t.Errorf("got length %d, want %d", len(secret), tt.want)
|
|
}
|
|
|
|
// Verify only alphanumeric characters
|
|
for _, c := range secret {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
|
|
t.Errorf("non-alphanumeric character found: %c", c)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: GenerateSecret produces unique values
|
|
func TestGenerateSecret_Uniqueness(t *testing.T) {
|
|
const numSecrets = 100
|
|
secrets := make(map[string]bool, numSecrets)
|
|
|
|
for i := 0; i < numSecrets; i++ {
|
|
secret, err := GenerateSecret(32)
|
|
if err != nil {
|
|
t.Fatalf("GenerateSecret failed: %v", err)
|
|
}
|
|
|
|
if secrets[secret] {
|
|
t.Errorf("duplicate secret generated: %s", secret)
|
|
}
|
|
secrets[secret] = true
|
|
}
|
|
|
|
if len(secrets) != numSecrets {
|
|
t.Errorf("expected %d unique secrets, got %d", numSecrets, len(secrets))
|
|
}
|
|
}
|
|
|
|
// Test: NewManager creates manager successfully
|
|
func TestNewManager(t *testing.T) {
|
|
m := NewManager()
|
|
if m == nil {
|
|
t.Fatal("NewManager returned nil")
|
|
}
|
|
if m.yq == nil {
|
|
t.Error("Manager.yq is nil")
|
|
}
|
|
}
|
|
|
|
// Test: EnsureSecretsFile creates secrets file with proper structure and permissions
|
|
func TestEnsureSecretsFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T, instancePath string)
|
|
wantErr bool
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "creates secrets file when not exists",
|
|
setupFunc: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "returns nil when secrets file exists",
|
|
setupFunc: func(t *testing.T, instancePath string) {
|
|
secretsPath := filepath.Join(instancePath, "secrets.yaml")
|
|
content := `# Wild Cloud Instance Secrets
|
|
cluster:
|
|
talosSecrets: ""
|
|
kubeconfig: ""
|
|
certManager:
|
|
cloudflare:
|
|
apiToken: ""
|
|
`
|
|
if err := storage.WriteFile(secretsPath, []byte(content), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "corrects permissions on existing file",
|
|
setupFunc: func(t *testing.T, instancePath string) {
|
|
secretsPath := filepath.Join(instancePath, "secrets.yaml")
|
|
content := `# Wild Cloud Instance Secrets
|
|
cluster:
|
|
talosSecrets: "existing-secret"
|
|
kubeconfig: ""
|
|
certManager:
|
|
cloudflare:
|
|
apiToken: ""
|
|
`
|
|
// Create with wrong permissions
|
|
if err := storage.WriteFile(secretsPath, []byte(content), 0644); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
instancePath := t.TempDir()
|
|
m := NewManager()
|
|
|
|
if tt.setupFunc != nil {
|
|
tt.setupFunc(t, instancePath)
|
|
}
|
|
|
|
err := m.EnsureSecretsFile(instancePath)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify secrets file exists
|
|
secretsPath := filepath.Join(instancePath, "secrets.yaml")
|
|
if !storage.FileExists(secretsPath) {
|
|
t.Error("secrets file not created")
|
|
}
|
|
|
|
// Verify permissions are 0600 (secure)
|
|
info, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("expected permissions 0600, got %o", info.Mode().Perm())
|
|
}
|
|
|
|
// Verify file has expected structure
|
|
content, err := storage.ReadFile(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read secrets: %v", err)
|
|
}
|
|
contentStr := string(content)
|
|
requiredFields := []string{"cluster:", "certManager:"}
|
|
for _, field := range requiredFields {
|
|
if !strings.Contains(contentStr, field) {
|
|
t.Errorf("secrets missing required field: %s", field)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: GetSecret retrieves secrets correctly
|
|
func TestGetSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
secretsYAML string
|
|
key string
|
|
want string
|
|
wantErr bool
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "get simple string value",
|
|
secretsYAML: `cluster:
|
|
talosSecrets: "my-secret-value"
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
want: "my-secret-value",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "get nested value with dot notation",
|
|
secretsYAML: `certManager:
|
|
cloudflare:
|
|
apiToken: "cf-token-12345"
|
|
`,
|
|
key: "certManager.cloudflare.apiToken",
|
|
want: "cf-token-12345",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "get non-existent key returns error",
|
|
secretsYAML: `cluster:
|
|
talosSecrets: "value"
|
|
`,
|
|
key: "nonexistent",
|
|
wantErr: true,
|
|
errContains: "secret not found",
|
|
},
|
|
{
|
|
name: "get empty string value returns error",
|
|
secretsYAML: `cluster:
|
|
talosSecrets: ""
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
wantErr: true,
|
|
errContains: "secret not found",
|
|
},
|
|
{
|
|
name: "get null value returns error",
|
|
secretsYAML: `cluster:
|
|
talosSecrets: null
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
wantErr: true,
|
|
errContains: "secret not found",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
if err := storage.WriteFile(secretsPath, []byte(tt.secretsYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, tt.key)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: GetSecret error cases
|
|
func TestGetSecret_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T) string
|
|
key string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "non-existent file",
|
|
setupFunc: func(t *testing.T) string {
|
|
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
|
},
|
|
key: "cluster.talosSecrets",
|
|
errContains: "secrets file not found",
|
|
},
|
|
{
|
|
name: "malformed yaml",
|
|
setupFunc: func(t *testing.T) string {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
content := `invalid: yaml: [[[`
|
|
if err := storage.WriteFile(secretsPath, []byte(content), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
return secretsPath
|
|
},
|
|
key: "cluster.talosSecrets",
|
|
errContains: "getting secret",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
secretsPath := tt.setupFunc(t)
|
|
m := NewManager()
|
|
|
|
_, err := m.GetSecret(secretsPath, tt.key)
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: GetSecret does not leak secrets in error messages
|
|
func TestGetSecret_NoSecretLeakage(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
secretValue := "super-secret-password-12345"
|
|
secretsYAML := `cluster:
|
|
talosSecrets: "` + secretValue + `"
|
|
`
|
|
if err := storage.WriteFile(secretsPath, []byte(secretsYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
|
|
// Try to get a non-existent key - error should not contain actual secret values
|
|
_, err := m.GetSecret(secretsPath, "nonexistent.key")
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
// Error message should not contain the secret value
|
|
if strings.Contains(err.Error(), secretValue) {
|
|
t.Errorf("error message leaked secret value: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test: SetSecret sets secrets correctly
|
|
func TestSetSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialYAML string
|
|
key string
|
|
value string
|
|
verifyFunc func(t *testing.T, secretsPath string)
|
|
}{
|
|
{
|
|
name: "set simple value",
|
|
initialYAML: `cluster:
|
|
talosSecrets: ""
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
value: "new-secret-value",
|
|
verifyFunc: func(t *testing.T, secretsPath string) {
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
|
|
if err != nil {
|
|
t.Fatalf("verify failed: %v", err)
|
|
}
|
|
if got != "new-secret-value" {
|
|
t.Errorf("got %q, want %q", got, "new-secret-value")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "set nested value",
|
|
initialYAML: `certManager:
|
|
cloudflare:
|
|
apiToken: ""
|
|
`,
|
|
key: "certManager.cloudflare.apiToken",
|
|
value: "cf-token-xyz",
|
|
verifyFunc: func(t *testing.T, secretsPath string) {
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, "certManager.cloudflare.apiToken")
|
|
if err != nil {
|
|
t.Fatalf("verify failed: %v", err)
|
|
}
|
|
if got != "cf-token-xyz" {
|
|
t.Errorf("got %q, want %q", got, "cf-token-xyz")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "update existing value",
|
|
initialYAML: `cluster:
|
|
talosSecrets: "old-secret"
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
value: "new-secret",
|
|
verifyFunc: func(t *testing.T, secretsPath string) {
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
|
|
if err != nil {
|
|
t.Fatalf("verify failed: %v", err)
|
|
}
|
|
if got != "new-secret" {
|
|
t.Errorf("got %q, want %q", got, "new-secret")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "create new nested path",
|
|
initialYAML: `cluster: {}
|
|
`,
|
|
key: "cluster.newSecret",
|
|
value: "newValue",
|
|
verifyFunc: func(t *testing.T, secretsPath string) {
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, "cluster.newSecret")
|
|
if err != nil {
|
|
t.Fatalf("verify failed: %v", err)
|
|
}
|
|
if got != "newValue" {
|
|
t.Errorf("got %q, want %q", got, "newValue")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "set value with special characters",
|
|
initialYAML: `cluster:
|
|
talosSecrets: ""
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
value: `special"quotes'and\backslashes`,
|
|
verifyFunc: func(t *testing.T, secretsPath string) {
|
|
m := NewManager()
|
|
got, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
|
|
if err != nil {
|
|
t.Fatalf("verify failed: %v", err)
|
|
}
|
|
if got != `special"quotes'and\backslashes` {
|
|
t.Errorf("got %q, want %q", got, `special"quotes'and\backslashes`)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
if err := m.SetSecret(secretsPath, tt.key, tt.value); err != nil {
|
|
t.Errorf("SetSecret failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify the value was set correctly
|
|
tt.verifyFunc(t, secretsPath)
|
|
|
|
// Verify permissions remain secure (0600)
|
|
info, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("permissions changed after SetSecret: got %o, want 0600", info.Mode().Perm())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: SetSecret error cases
|
|
func TestSetSecret_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T) string
|
|
key string
|
|
value string
|
|
errContains string
|
|
}{
|
|
{
|
|
name: "non-existent file",
|
|
setupFunc: func(t *testing.T) string {
|
|
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
|
},
|
|
key: "cluster.talosSecrets",
|
|
value: "secret",
|
|
errContains: "secrets file not found",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
secretsPath := tt.setupFunc(t)
|
|
m := NewManager()
|
|
|
|
err := m.SetSecret(secretsPath, tt.key, tt.value)
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !strings.Contains(err.Error(), tt.errContains) {
|
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: SetSecret with concurrent access
|
|
func TestSetSecret_ConcurrentAccess(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
initialYAML := `counter: "0"
|
|
`
|
|
if err := storage.WriteFile(secretsPath, []byte(initialYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
const numGoroutines = 10
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, numGoroutines)
|
|
|
|
// Launch multiple goroutines trying to write different values
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(val int) {
|
|
defer wg.Done()
|
|
key := "counter"
|
|
value := string(rune('0' + val))
|
|
if err := m.SetSecret(secretsPath, key, value); err != nil {
|
|
errors <- err
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
// Check if any errors occurred
|
|
for err := range errors {
|
|
t.Errorf("concurrent write error: %v", err)
|
|
}
|
|
|
|
// Verify permissions remain secure after concurrent access
|
|
info, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("permissions changed after concurrent writes: got %o, want 0600", info.Mode().Perm())
|
|
}
|
|
|
|
// Verify we can read a value (should be one of the written values)
|
|
value, err := m.GetSecret(secretsPath, "counter")
|
|
if err != nil {
|
|
t.Errorf("failed to read value after concurrent writes: %v", err)
|
|
}
|
|
if value == "" || value == "null" {
|
|
t.Error("counter value is empty after concurrent writes")
|
|
}
|
|
}
|
|
|
|
// Test: EnsureSecret generates and sets secret only when not set
|
|
func TestEnsureSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialYAML string
|
|
key string
|
|
length int
|
|
expectNew bool
|
|
}{
|
|
{
|
|
name: "generates secret when empty string",
|
|
initialYAML: `cluster:
|
|
talosSecrets: ""
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
length: 32,
|
|
expectNew: true,
|
|
},
|
|
{
|
|
name: "generates secret when null",
|
|
initialYAML: `cluster:
|
|
talosSecrets: null
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
length: 32,
|
|
expectNew: true,
|
|
},
|
|
{
|
|
name: "does not generate when secret exists",
|
|
initialYAML: `cluster:
|
|
talosSecrets: "existing-secret"
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
length: 32,
|
|
expectNew: false,
|
|
},
|
|
{
|
|
name: "generates secret for non-existent key",
|
|
initialYAML: `cluster: {}
|
|
`,
|
|
key: "cluster.newSecret",
|
|
length: 64,
|
|
expectNew: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
|
|
// Get initial value if exists
|
|
initialVal, _ := m.GetSecret(secretsPath, tt.key)
|
|
|
|
// Call EnsureSecret
|
|
secret, err := m.EnsureSecret(secretsPath, tt.key, tt.length)
|
|
if err != nil {
|
|
t.Errorf("EnsureSecret failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify secret is returned
|
|
if secret == "" {
|
|
t.Error("EnsureSecret returned empty secret")
|
|
}
|
|
|
|
// Verify length
|
|
if tt.expectNew && len(secret) != tt.length {
|
|
t.Errorf("expected secret length %d, got %d", tt.length, len(secret))
|
|
}
|
|
|
|
// Get final value
|
|
finalVal, err := m.GetSecret(secretsPath, tt.key)
|
|
if err != nil {
|
|
t.Fatalf("GetSecret failed: %v", err)
|
|
}
|
|
|
|
if tt.expectNew {
|
|
// Should have generated new secret
|
|
if initialVal != "" && finalVal == initialVal {
|
|
t.Errorf("expected new secret, got same value: %q", finalVal)
|
|
}
|
|
} else {
|
|
// Should have kept existing secret
|
|
if finalVal != initialVal {
|
|
t.Errorf("expected to keep existing secret %q, got %q", initialVal, finalVal)
|
|
}
|
|
}
|
|
|
|
// Call EnsureSecret again - should be idempotent
|
|
secret2, err := m.EnsureSecret(secretsPath, tt.key, tt.length)
|
|
if err != nil {
|
|
t.Errorf("second EnsureSecret failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// Secret should not change on second call
|
|
if secret2 != secret {
|
|
t.Errorf("secret changed on second ensure: %q -> %q", secret, secret2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: GenerateAndStoreSecret convenience function
|
|
func TestGenerateAndStoreSecret(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
initialYAML := `cluster:
|
|
talosSecrets: ""
|
|
`
|
|
if err := storage.WriteFile(secretsPath, []byte(initialYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
|
|
// Generate and store secret
|
|
secret, err := m.GenerateAndStoreSecret(secretsPath, "cluster.talosSecrets")
|
|
if err != nil {
|
|
t.Fatalf("GenerateAndStoreSecret failed: %v", err)
|
|
}
|
|
|
|
// Verify secret length matches default
|
|
if len(secret) != DefaultSecretLength {
|
|
t.Errorf("expected length %d, got %d", DefaultSecretLength, len(secret))
|
|
}
|
|
|
|
// Verify secret is stored
|
|
stored, err := m.GetSecret(secretsPath, "cluster.talosSecrets")
|
|
if err != nil {
|
|
t.Fatalf("GetSecret failed: %v", err)
|
|
}
|
|
|
|
if stored != secret {
|
|
t.Errorf("stored secret %q does not match returned secret %q", stored, secret)
|
|
}
|
|
}
|
|
|
|
// Test: DeleteSecret removes secrets correctly
|
|
func TestDeleteSecret(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
initialYAML string
|
|
key string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "delete existing secret",
|
|
initialYAML: `cluster:
|
|
talosSecrets: "secret-to-delete"
|
|
kubeconfig: "other-secret"
|
|
`,
|
|
key: "cluster.talosSecrets",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "delete nested secret",
|
|
initialYAML: `certManager:
|
|
cloudflare:
|
|
apiToken: "token-to-delete"
|
|
`,
|
|
key: "certManager.cloudflare.apiToken",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
|
|
if err := storage.WriteFile(secretsPath, []byte(tt.initialYAML), 0600); err != nil {
|
|
t.Fatalf("setup failed: %v", err)
|
|
}
|
|
|
|
m := NewManager()
|
|
|
|
// Verify secret exists before deletion
|
|
_, err := m.GetSecret(secretsPath, tt.key)
|
|
if err != nil {
|
|
t.Fatalf("secret should exist before deletion: %v", err)
|
|
}
|
|
|
|
// Delete secret
|
|
err = m.DeleteSecret(secretsPath, tt.key)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
// Verify secret no longer exists
|
|
_, err = m.GetSecret(secretsPath, tt.key)
|
|
if err == nil {
|
|
t.Error("secret should not exist after deletion")
|
|
}
|
|
|
|
// Verify permissions remain secure (0600)
|
|
info, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("permissions changed after DeleteSecret: got %o, want 0600", info.Mode().Perm())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test: DeleteSecret error cases
|
|
func TestDeleteSecret_Errors(t *testing.T) {
|
|
t.Run("non-existent file", func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "nonexistent.yaml")
|
|
|
|
m := NewManager()
|
|
err := m.DeleteSecret(secretsPath, "cluster.talosSecrets")
|
|
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !strings.Contains(err.Error(), "secrets file not found") {
|
|
t.Errorf("error %q does not contain 'secrets file not found'", err.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test: File permissions are always 0600
|
|
func TestEnsureSecretsFile_FilePermissions(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
m := NewManager()
|
|
|
|
if err := m.EnsureSecretsFile(tempDir); err != nil {
|
|
t.Fatalf("EnsureSecretsFile failed: %v", err)
|
|
}
|
|
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
info, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets file: %v", err)
|
|
}
|
|
|
|
// Verify file has 0600 permissions (read/write for owner only)
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("expected permissions 0600, got %o", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// Test: Idempotent secrets creation
|
|
func TestEnsureSecretsFile_Idempotent(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
m := NewManager()
|
|
|
|
// First call creates secrets
|
|
if err := m.EnsureSecretsFile(tempDir); err != nil {
|
|
t.Fatalf("first EnsureSecretsFile failed: %v", err)
|
|
}
|
|
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
firstContent, err := storage.ReadFile(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read secrets: %v", err)
|
|
}
|
|
|
|
// Second call should not modify secrets
|
|
if err := m.EnsureSecretsFile(tempDir); err != nil {
|
|
t.Fatalf("second EnsureSecretsFile failed: %v", err)
|
|
}
|
|
|
|
secondContent, err := storage.ReadFile(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read secrets: %v", err)
|
|
}
|
|
|
|
if string(firstContent) != string(secondContent) {
|
|
t.Error("secrets content changed on second call")
|
|
}
|
|
}
|
|
|
|
// Test: Secrets structure contains required fields
|
|
func TestEnsureSecretsFile_RequiredFields(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
m := NewManager()
|
|
|
|
if err := m.EnsureSecretsFile(tempDir); err != nil {
|
|
t.Fatalf("EnsureSecretsFile failed: %v", err)
|
|
}
|
|
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
content, err := storage.ReadFile(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read secrets: %v", err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
requiredFields := []string{
|
|
"cluster:",
|
|
"talosSecrets:",
|
|
"kubeconfig:",
|
|
"certManager:",
|
|
"cloudflare:",
|
|
"apiToken:",
|
|
}
|
|
|
|
for _, field := range requiredFields {
|
|
if !strings.Contains(contentStr, field) {
|
|
t.Errorf("secrets missing required field: %s", field)
|
|
}
|
|
}
|
|
|
|
// Verify warning comment exists
|
|
if !strings.Contains(contentStr, "WARNING") || !strings.Contains(contentStr, "sensitive") {
|
|
t.Error("secrets file missing security warning comment")
|
|
}
|
|
}
|
|
|
|
// Test: Secrets are more restrictive than config
|
|
func TestSecretsPermissions_MoreRestrictiveThanConfig(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
secretsPath := filepath.Join(tempDir, "secrets.yaml")
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
// Create secrets file
|
|
m := NewManager()
|
|
if err := m.EnsureSecretsFile(tempDir); err != nil {
|
|
t.Fatalf("EnsureSecretsFile failed: %v", err)
|
|
}
|
|
|
|
// Create config file (typically 0644)
|
|
configContent := `baseDomain: "example.com"`
|
|
if err := storage.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
// Check permissions
|
|
secretsInfo, err := os.Stat(secretsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat secrets: %v", err)
|
|
}
|
|
|
|
configInfo, err := os.Stat(configPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat config: %v", err)
|
|
}
|
|
|
|
secretsPerm := secretsInfo.Mode().Perm()
|
|
configPerm := configInfo.Mode().Perm()
|
|
|
|
// Secrets (0600) should be more restrictive than config (0644)
|
|
if secretsPerm >= configPerm {
|
|
t.Errorf("secrets permissions %o should be more restrictive than config %o", secretsPerm, configPerm)
|
|
}
|
|
|
|
// Secrets should not be group or world readable
|
|
if secretsPerm&0077 != 0 {
|
|
t.Errorf("secrets file should not have group/world permissions, got %o", secretsPerm)
|
|
}
|
|
}
|