Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 32 additions & 22 deletions cmd/cloudstic/cmd_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"io"
"os"
"runtime"
"strings"

cloudstic "github.com/cloudstic/cli"
Expand Down Expand Up @@ -510,7 +509,6 @@ func configureStoreEncryptionSelection(

func (r *runner) promptSecretReference(storeName, secretLabel, defaultEnvName, defaultAccount string) (string, error) {
return promptSecretReferenceWithFns(
runtime.GOOS,
storeName,
secretLabel,
defaultEnvName,
Expand All @@ -519,29 +517,27 @@ func (r *runner) promptSecretReference(storeName, secretLabel, defaultEnvName, d
r.promptLine,
r.promptSecret,
os.LookupEnv,
nativeSecretExists,
saveSecretToNativeStore,
profileSecretResolver,
)
}

func promptSecretReferenceWithFns(
goos, storeName, secretLabel, defaultEnvName, defaultAccount string,
storeName, secretLabel, defaultEnvName, defaultAccount string,
promptSelect func(string, []string) (string, error),
promptLine func(string, string) (string, error),
promptSecret func(string) (string, error),
lookupEnv func(string) (string, bool),
nativeSecretExists func(context.Context, string, string) (bool, error),
writeNativeSecret func(context.Context, string, string, string) error,
resolver *secretref.Resolver,
) (string, error) {
keychainRef := func() (string, error) {
service := "cloudstic/store/" + storeName
account := defaultAccount
exists, err := nativeSecretExists(context.Background(), service, account)
writableBackends := resolver.WritableBackends()
nativeRef := func(backend secretref.WritableBackend) (string, error) {
ref := backend.DefaultRef(storeName, defaultAccount)
exists, err := resolver.Exists(context.Background(), ref)
if err != nil {
return "", err
}
if exists {
return "keychain://" + service + "/" + account, nil
return ref, nil
}
secretValue, err := promptSecret("Secret value")
if err != nil {
Expand All @@ -550,39 +546,53 @@ func promptSecretReferenceWithFns(
if secretValue == "" {
return "", fmt.Errorf("secret value cannot be empty")
}
if err := writeNativeSecret(context.Background(), service, account, secretValue); err != nil {
if err := resolver.Store(context.Background(), ref, secretValue); err != nil {
return "", err
}
return "keychain://" + service + "/" + account, nil
return ref, nil
}

if goos == "darwin" {
if len(writableBackends) > 0 {
options := []string{"Environment variable (env://)"}
backendByOption := map[string]secretref.WritableBackend{}
for _, backend := range writableBackends {
option := fmt.Sprintf("%s (%s://)", backend.DisplayName(), backend.Scheme())
options = append(options, option)
backendByOption[option] = backend
}
picked, err := promptSelect(
fmt.Sprintf("Where should %s be stored?", secretLabel),
[]string{"Environment variable (env://)", "macOS Keychain (keychain://)"},
options,
)
if err != nil {
return "", err
}
if strings.HasPrefix(picked, "macOS Keychain") {
return keychainRef()
if backend, ok := backendByOption[picked]; ok {
return nativeRef(backend)
}
}

envName, err := promptLine("Env var name", defaultEnvName)
if err != nil {
return "", err
}
if _, ok := lookupEnv(envName); !ok && goos == "darwin" {
if _, ok := lookupEnv(envName); !ok && len(writableBackends) > 0 {
options := []string{"Keep environment variable reference (env://)"}
backendByOption := map[string]secretref.WritableBackend{}
for _, backend := range writableBackends {
option := fmt.Sprintf("Store in %s instead (%s://)", backend.DisplayName(), backend.Scheme())
options = append(options, option)
backendByOption[option] = backend
}
picked, err := promptSelect(
fmt.Sprintf("Environment variable %q is not set in this shell", envName),
[]string{"Keep environment variable reference (env://)", "Store in macOS Keychain instead (keychain://)"},
options,
)
if err != nil {
return "", err
}
if strings.HasPrefix(picked, "Store in macOS Keychain") {
return keychainRef()
if backend, ok := backendByOption[picked]; ok {
return nativeRef(backend)
}
}
return envRef(envName), nil
Expand Down
136 changes: 90 additions & 46 deletions cmd/cloudstic/cmd_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,36 @@ import (
"testing"

cloudstic "github.com/cloudstic/cli"
"github.com/cloudstic/cli/internal/secretref"
"github.com/cloudstic/cli/pkg/keychain"
)

type writableBackendStub struct {
scheme string
displayName string
defaultRef string
exists func(context.Context, secretref.Ref) (bool, error)
store func(context.Context, secretref.Ref, string) error
}

func (b writableBackendStub) Resolve(context.Context, secretref.Ref) (string, error) { return "", nil }
func (b writableBackendStub) Scheme() string { return b.scheme }
func (b writableBackendStub) DisplayName() string { return b.displayName }
func (b writableBackendStub) WriteSupported() bool { return true }
func (b writableBackendStub) DefaultRef(string, string) string { return b.defaultRef }
func (b writableBackendStub) Exists(ctx context.Context, ref secretref.Ref) (bool, error) {
if b.exists == nil {
return false, nil
}
return b.exists(ctx, ref)
}
func (b writableBackendStub) Store(ctx context.Context, ref secretref.Ref, value string) error {
if b.store == nil {
return nil
}
return b.store(ctx, ref, value)
}

func TestRunStoreNewAndListAndShow(t *testing.T) {
tmpDir := t.TempDir()
profilesPath := filepath.Join(tmpDir, "profiles.yaml")
Expand Down Expand Up @@ -753,8 +780,25 @@ func TestRunStoreNew_WithSecretRefFlags(t *testing.T) {
}

func TestPromptSecretReferenceWithFns_DarwinKeychain(t *testing.T) {
resolver := secretref.NewResolver(map[string]secretref.Backend{
"keychain": writableBackendStub{
scheme: "keychain",
displayName: "macOS Keychain",
defaultRef: "keychain://cloudstic/store/prod-store/password",
exists: func(context.Context, secretref.Ref) (bool, error) { return false, nil },
store: func(_ context.Context, ref secretref.Ref, value string) error {
if ref.Raw != "keychain://cloudstic/store/prod-store/password" {
t.Fatalf("ref=%q", ref.Raw)
}
if value != "super-secret" {
t.Fatalf("value=%q", value)
}
return nil
},
},
})

gotRef, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -763,19 +807,7 @@ func TestPromptSecretReferenceWithFns_DarwinKeychain(t *testing.T) {
func(label, def string) (string, error) { return def, nil },
func(_ string) (string, error) { return "super-secret", nil },
func(string) (string, bool) { return "", false },
func(context.Context, string, string) (bool, error) { return false, nil },
func(_ context.Context, service, account, value string) error {
if service != "cloudstic/store/prod-store" {
t.Fatalf("service=%q", service)
}
if account != "password" {
t.Fatalf("account=%q", account)
}
if value != "super-secret" {
t.Fatalf("value=%q", value)
}
return nil
},
resolver,
)
if err != nil {
t.Fatalf("promptSecretReferenceWithFns: %v", err)
Expand All @@ -786,8 +818,8 @@ func TestPromptSecretReferenceWithFns_DarwinKeychain(t *testing.T) {
}

func TestPromptSecretReferenceWithFns_EnvFallback(t *testing.T) {
resolver := secretref.NewResolver(nil)
gotRef, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -804,14 +836,7 @@ func TestPromptSecretReferenceWithFns_EnvFallback(t *testing.T) {
return "", nil
},
func(string) (string, bool) { return "", true },
func(context.Context, string, string) (bool, error) {
t.Fatal("nativeSecretExists should not be called")
return false, nil
},
func(context.Context, string, string, string) error {
t.Fatal("writeNativeSecret should not be called")
return nil
},
resolver,
)
if err != nil {
t.Fatalf("promptSecretReferenceWithFns: %v", err)
Expand All @@ -822,8 +847,16 @@ func TestPromptSecretReferenceWithFns_EnvFallback(t *testing.T) {
}

func TestPromptSecretReferenceWithFns_KeychainWriteError(t *testing.T) {
resolver := secretref.NewResolver(map[string]secretref.Backend{
"keychain": writableBackendStub{
scheme: "keychain",
displayName: "macOS Keychain",
defaultRef: "keychain://cloudstic/store/prod-store/password",
exists: func(context.Context, secretref.Ref) (bool, error) { return false, nil },
store: func(context.Context, secretref.Ref, string) error { return errors.New("write failed") },
},
})
_, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -832,8 +865,7 @@ func TestPromptSecretReferenceWithFns_KeychainWriteError(t *testing.T) {
func(_ string, def string) (string, error) { return def, nil },
func(_ string) (string, error) { return "secret", nil },
func(string) (string, bool) { return "", false },
func(context.Context, string, string) (bool, error) { return false, nil },
func(context.Context, string, string, string) error { return errors.New("write failed") },
resolver,
)
if err == nil {
t.Fatal("expected error")
Expand All @@ -844,8 +876,10 @@ func TestPromptSecretReferenceWithFns_KeychainWriteError(t *testing.T) {
}

func TestPromptSecretReferenceWithFns_EmptySecret(t *testing.T) {
resolver := secretref.NewResolver(map[string]secretref.Backend{
"keychain": writableBackendStub{scheme: "keychain", displayName: "macOS Keychain", defaultRef: "keychain://cloudstic/store/prod-store/password"},
})
_, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -854,8 +888,7 @@ func TestPromptSecretReferenceWithFns_EmptySecret(t *testing.T) {
func(_ string, def string) (string, error) { return def, nil },
func(_ string) (string, error) { return "", nil },
func(string) (string, bool) { return "", false },
func(context.Context, string, string) (bool, error) { return false, nil },
func(context.Context, string, string, string) error { return nil },
resolver,
)
if err == nil {
t.Fatal("expected error")
Expand All @@ -866,8 +899,24 @@ func TestPromptSecretReferenceWithFns_EmptySecret(t *testing.T) {
}

func TestPromptSecretReferenceWithFns_DarwinKeychainAdoptsExisting(t *testing.T) {
resolver := secretref.NewResolver(map[string]secretref.Backend{
"keychain": writableBackendStub{
scheme: "keychain",
displayName: "macOS Keychain",
defaultRef: "keychain://cloudstic/store/prod-store/password",
exists: func(_ context.Context, ref secretref.Ref) (bool, error) {
if ref.Raw != "keychain://cloudstic/store/prod-store/password" {
t.Fatalf("ref=%q", ref.Raw)
}
return true, nil
},
store: func(context.Context, secretref.Ref, string) error {
t.Fatal("store should not be called when secret exists")
return nil
},
},
})
gotRef, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -879,19 +928,7 @@ func TestPromptSecretReferenceWithFns_DarwinKeychainAdoptsExisting(t *testing.T)
return "", nil
},
func(string) (string, bool) { return "", false },
func(_ context.Context, service, account string) (bool, error) {
if service != "cloudstic/store/prod-store" {
t.Fatalf("service=%q", service)
}
if account != "password" {
t.Fatalf("account=%q", account)
}
return true, nil
},
func(context.Context, string, string, string) error {
t.Fatal("writeNativeSecret should not be called when key exists")
return nil
},
resolver,
)
if err != nil {
t.Fatalf("promptSecretReferenceWithFns: %v", err)
Expand All @@ -903,8 +940,16 @@ func TestPromptSecretReferenceWithFns_DarwinKeychainAdoptsExisting(t *testing.T)

func TestPromptSecretReferenceWithFns_DarwinEnvUnsetSwitchesToKeychain(t *testing.T) {
selectCall := 0
resolver := secretref.NewResolver(map[string]secretref.Backend{
"keychain": writableBackendStub{
scheme: "keychain",
displayName: "macOS Keychain",
defaultRef: "keychain://cloudstic/store/prod-store/password",
exists: func(context.Context, secretref.Ref) (bool, error) { return false, nil },
store: func(context.Context, secretref.Ref, string) error { return nil },
},
})
gotRef, err := promptSecretReferenceWithFns(
"darwin",
"prod-store",
"repository password",
"CLOUDSTIC_PASSWORD",
Expand All @@ -924,8 +969,7 @@ func TestPromptSecretReferenceWithFns_DarwinEnvUnsetSwitchesToKeychain(t *testin
},
func(_ string) (string, error) { return "secret-value", nil },
func(string) (string, bool) { return "", false },
func(context.Context, string, string) (bool, error) { return false, nil },
func(context.Context, string, string, string) error { return nil },
resolver,
)
if err != nil {
t.Fatalf("promptSecretReferenceWithFns: %v", err)
Expand Down
Loading
Loading