Skip to content

Commit c97c60d

Browse files
authored
Add --secrets-provider keychain (#30)
Add a flag to allow a secrets provider to be specified. Currently `basic` is the only option. Some other cleanups in the code.
1 parent d800ffc commit c97c60d

File tree

10 files changed

+416
-72
lines changed

10 files changed

+416
-72
lines changed

cmd/vt/common.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package main
22

33
import (
4+
"fmt"
5+
46
"github.com/spf13/cobra"
7+
8+
"github.com/stacklok/vibetool/pkg/secrets"
59
)
610

711
// AddOIDCFlags adds OIDC validation flags to the provided command.
@@ -30,3 +34,19 @@ func IsOIDCEnabled(cmd *cobra.Command) bool {
3034

3135
return jwksURL != "" || issuer != ""
3236
}
37+
38+
// GetSecretsProviderType returns the secrets provider type from the command flags
39+
func GetSecretsProviderType(cmd *cobra.Command) (secrets.ManagerType, error) {
40+
provider, err := cmd.Flags().GetString("secrets-provider")
41+
if err != nil {
42+
return "", fmt.Errorf("failed to get secrets-provider flag: %w", err)
43+
}
44+
45+
switch provider {
46+
case string(secrets.BasicType), "":
47+
return secrets.BasicType, nil
48+
default:
49+
// TODO: auto-generate the set of valid values.
50+
return "", fmt.Errorf("invalid secrets provider type: %s (valid types: basic)", provider)
51+
}
52+
}

cmd/vt/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var versionCmd = &cobra.Command{
3535
func init() {
3636
// Add persistent flags
3737
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode")
38+
rootCmd.PersistentFlags().String("secrets-provider", "basic", "Secrets provider to use (basic)")
3839

3940
// Add subcommands
4041
rootCmd.AddCommand(runCmd)

cmd/vt/run_common.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,16 @@ func RunMCPServer(ctx context.Context, cmd *cobra.Command, options RunOptions) e
104104

105105
// If any secrets are specified, attempt to load them, and add to list of environment variables.
106106
if len(options.Secrets) > 0 {
107-
secretManager, err := secrets.CreateDefaultSecretsManager()
107+
providerType, err := GetSecretsProviderType(cmd)
108108
if err != nil {
109+
return fmt.Errorf("error determining secrets provider type: %w", err)
110+
}
111+
112+
secretManager, err := secrets.CreateSecretManager(providerType)
113+
if err != nil {
114+
if strings.Contains(err.Error(), "incorrect password") {
115+
return fmt.Errorf("error: %v", err)
116+
}
109117
return fmt.Errorf("error instantiating secret manager %v", err)
110118
}
111119
secretVariables, err := environment.ParseSecretParameters(options.Secrets, secretManager)

cmd/vt/secret.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"os"
5+
46
"github.com/spf13/cobra"
57

68
"github.com/stacklok/vibetool/pkg/secrets"
@@ -37,7 +39,13 @@ func newSecretSetCommand() *cobra.Command {
3739
return
3840
}
3941

40-
manager, err := secrets.CreateDefaultSecretsManager()
42+
providerType, err := GetSecretsProviderType(cmd)
43+
if err != nil {
44+
cmd.Printf("Error: %v\n", err)
45+
os.Exit(1)
46+
}
47+
48+
manager, err := secrets.CreateSecretManager(providerType)
4149
if err != nil {
4250
cmd.Printf("Failed to create secrets manager: %v\n", err)
4351
return
@@ -67,7 +75,13 @@ func newSecretGetCommand() *cobra.Command {
6775
return
6876
}
6977

70-
manager, err := secrets.CreateDefaultSecretsManager()
78+
providerType, err := GetSecretsProviderType(cmd)
79+
if err != nil {
80+
cmd.Printf("Error: %v\n", err)
81+
os.Exit(1)
82+
}
83+
84+
manager, err := secrets.CreateSecretManager(providerType)
7185
if err != nil {
7286
cmd.Printf("Failed to create secrets manager: %v\n", err)
7387
return
@@ -97,7 +111,13 @@ func newSecretDeleteCommand() *cobra.Command {
97111
return
98112
}
99113

100-
manager, err := secrets.CreateDefaultSecretsManager()
114+
providerType, err := GetSecretsProviderType(cmd)
115+
if err != nil {
116+
cmd.Printf("Error: %v\n", err)
117+
os.Exit(1)
118+
}
119+
120+
manager, err := secrets.CreateSecretManager(providerType)
101121
if err != nil {
102122
cmd.Printf("Failed to create secrets manager: %v\n", err)
103123
return
@@ -119,7 +139,13 @@ func newSecretListCommand() *cobra.Command {
119139
Short: "List all available secrets",
120140
Args: cobra.NoArgs,
121141
Run: func(cmd *cobra.Command, _ []string) {
122-
manager, err := secrets.CreateDefaultSecretsManager()
142+
providerType, err := GetSecretsProviderType(cmd)
143+
if err != nil {
144+
cmd.Printf("Error: %v\n", err)
145+
os.Exit(1)
146+
}
147+
148+
manager, err := secrets.CreateSecretManager(providerType)
123149
if err != nil {
124150
cmd.Printf("Failed to create secrets manager: %v\n", err)
125151
return

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/gofrs/flock v0.12.1
1010
github.com/google/uuid v1.6.0
1111
github.com/stretchr/testify v1.10.0
12+
golang.org/x/sync v0.12.0
1213
gopkg.in/yaml.v3 v3.0.1
1314
k8s.io/apimachinery v0.32.3
1415
)
@@ -36,7 +37,7 @@ require (
3637
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
3738
golang.org/x/net v0.35.0 // indirect
3839
golang.org/x/oauth2 v0.23.0 // indirect
39-
golang.org/x/term v0.29.0 // indirect
40+
golang.org/x/term v0.30.0 // indirect
4041
golang.org/x/text v0.22.0 // indirect
4142
golang.org/x/time v0.7.0 // indirect
4243
google.golang.org/protobuf v1.36.5 // indirect
@@ -87,6 +88,6 @@ require (
8788
go.opentelemetry.io/otel/trace v1.35.0 // indirect
8889
golang.org/x/crypto v0.33.0 // indirect
8990
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
90-
golang.org/x/sys v0.30.0 // indirect
91+
golang.org/x/sys v0.31.0 // indirect
9192
k8s.io/client-go v0.32.3
9293
)

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,17 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
196196
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
197197
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
198198
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
199+
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
200+
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
199201
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
200202
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
201203
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
202204
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
203205
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
204-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
205-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
206-
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
207-
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
206+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
207+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
208+
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
209+
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
208210
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
209211
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
210212
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=

pkg/secrets/basic.go

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,15 @@ import (
77
"log"
88
"os"
99
"path"
10-
"sync"
1110

12-
"github.com/adrg/xdg"
11+
"golang.org/x/sync/syncmap"
1312
)
1413

1514
// BasicManager is a simple secrets manager that stores secrets in an
1615
// unencrypted file. This is for testing/development purposes only.
1716
type BasicManager struct {
1817
filePath string
19-
secrets map[string]string
20-
mu sync.RWMutex // Protects concurrent access to secrets map
18+
secrets syncmap.Map // Thread-safe map for storing secrets
2119
}
2220

2321
// GetSecret retrieves a secret from the secret store.
@@ -26,14 +24,11 @@ func (b *BasicManager) GetSecret(name string) (string, error) {
2624
return "", errors.New("secret name cannot be empty")
2725
}
2826

29-
b.mu.RLock()
30-
defer b.mu.RUnlock()
31-
32-
value, ok := b.secrets[name]
27+
value, ok := b.secrets.Load(name)
3328
if !ok {
3429
return "", fmt.Errorf("secret not found: %s", name)
3530
}
36-
return value, nil
31+
return value.(string), nil
3732
}
3833

3934
// SetSecret stores a secret in the secret store.
@@ -42,10 +37,7 @@ func (b *BasicManager) SetSecret(name, value string) error {
4237
return errors.New("secret name cannot be empty")
4338
}
4439

45-
b.mu.Lock()
46-
defer b.mu.Unlock()
47-
48-
b.secrets[name] = value
40+
b.secrets.Store(name, value)
4941
return b.updateFile()
5042
}
5143

@@ -55,32 +47,46 @@ func (b *BasicManager) DeleteSecret(name string) error {
5547
return errors.New("secret name cannot be empty")
5648
}
5749

58-
b.mu.Lock()
59-
defer b.mu.Unlock()
60-
61-
if _, exists := b.secrets[name]; !exists {
50+
// Check if the secret exists first
51+
_, ok := b.secrets.Load(name)
52+
if !ok {
6253
return fmt.Errorf("cannot delete non-existent secret: %s", name)
6354
}
6455

65-
delete(b.secrets, name)
56+
b.secrets.Delete(name)
6657
return b.updateFile()
6758
}
6859

6960
// ListSecrets returns a list of all secret names stored in the manager.
7061
func (b *BasicManager) ListSecrets() ([]string, error) {
71-
b.mu.RLock()
72-
defer b.mu.RUnlock()
62+
var secretNames []string
7363

74-
secretNames := make([]string, 0, len(b.secrets))
75-
for name := range b.secrets {
76-
secretNames = append(secretNames, name)
77-
}
64+
b.secrets.Range(func(key, _ interface{}) bool {
65+
secretNames = append(secretNames, key.(string))
66+
return true
67+
})
7868

7969
return secretNames, nil
8070
}
8171

72+
// Cleanup removes all secrets managed by this manager.
73+
func (b *BasicManager) Cleanup() error {
74+
// Create a new empty syncmap.Map
75+
b.secrets = syncmap.Map{}
76+
77+
// Update the file to reflect the empty state
78+
return b.updateFile()
79+
}
80+
8281
func (b *BasicManager) updateFile() error {
83-
contents, err := json.Marshal(fileStructure{Secrets: b.secrets})
82+
// Convert syncmap.Map to map[string]string for JSON marshaling
83+
secretsMap := make(map[string]string)
84+
b.secrets.Range(func(key, value interface{}) bool {
85+
secretsMap[key.(string)] = value.(string)
86+
return true
87+
})
88+
89+
contents, err := json.Marshal(fileStructure{Secrets: secretsMap})
8490
if err != nil {
8591
return fmt.Errorf("failed to marshal secrets: %w", err)
8692
}
@@ -103,16 +109,8 @@ type fileStructure struct {
103109
Secrets map[string]string `json:"secrets"`
104110
}
105111

106-
// BasicManagerFactory is an implementation of the ManagerFactory interface for BasicManager.
107-
type BasicManagerFactory struct{}
108-
109-
// Build creates an instance of BasicManager.
110-
func (BasicManagerFactory) Build(config map[string]interface{}) (Manager, error) {
111-
filePath, ok := config["secretsFile"].(string)
112-
if !ok {
113-
return nil, errors.New("secretsFile is required")
114-
}
115-
112+
// NewBasicManager creates an instance of BasicManager.
113+
func NewBasicManager(filePath string) (Manager, error) {
116114
// Add warning for production use
117115
if os.Getenv("ENVIRONMENT") == "production" {
118116
log.Println("WARNING: BasicManager is not secure for production use")
@@ -131,32 +129,25 @@ func (BasicManagerFactory) Build(config map[string]interface{}) (Manager, error)
131129
return nil, fmt.Errorf("failed to stat secrets file: %w", err)
132130
}
133131

134-
var secrets map[string]string
135-
if stat.Size() == 0 {
136-
secrets = make(map[string]string)
137-
} else {
132+
// Create a new BasicManager with an empty syncmap.Map
133+
manager := &BasicManager{
134+
filePath: filePath,
135+
secrets: syncmap.Map{},
136+
}
137+
138+
// If the file is not empty, load the secrets into the syncmap.Map
139+
if stat.Size() > 0 {
138140
var contents fileStructure
139141
err := json.NewDecoder(secretsFile).Decode(&contents)
140142
if err != nil {
141143
return nil, fmt.Errorf("failed to decode secrets file: %w", err)
142144
}
143-
secrets = contents.Secrets
144-
}
145145

146-
return &BasicManager{
147-
filePath: filePath,
148-
secrets: secrets,
149-
mu: sync.RWMutex{},
150-
}, nil
151-
}
152-
153-
// CreateDefaultSecretsManager creates a secret manager instance with a default config.
154-
// TODO: Remove once we support more than one provider
155-
func CreateDefaultSecretsManager() (Manager, error) {
156-
// TODO: make this configurable?
157-
secretsPath, err := xdg.DataFile("vibetool/secrets")
158-
if err != nil {
159-
return nil, fmt.Errorf("unable to access secrets file path %v", err)
146+
// Store each secret in the syncmap.Map
147+
for key, value := range contents.Secrets {
148+
manager.secrets.Store(key, value)
149+
}
160150
}
161-
return BasicManagerFactory{}.Build(map[string]any{"secretsFile": secretsPath})
151+
152+
return manager, nil
162153
}

0 commit comments

Comments
 (0)