package secrets import ( "context" "encoding/json" "fmt" "io/fs" "path/filepath" "sync" "time" "github.com/mitchellh/mapstructure" "gopkg.in/yaml.v2" ) type ConfigFileHandle struct{} func (h *ConfigFileHandle) Reference() string { return "NOOP" } var _ Handle = (*ConfigFileHandle)(nil) // ConfigFileClient returns secrets from a JSON or YAML configuration // file. This mode isn't as secure as using Vault or some other secret // management service but can be useful for users who don't have access // to such a service. // // Writes to this secret client will silently succeed while doing // nothing. type ConfigFileClient struct { c chan Renewal cfg map[string]any } var _ Client = (*ConfigFileClient)(nil) var _ ClientManager = (*ConfigFileClient)(nil) // NewConfigFileClient creates a new ConfigFileClient by loading a named // config file from a filesystem and unmarshalling it. The config file // can be in JSON or YAML format, determined by a .json, .yaml, or .yml // extension. The configuration must be nested within a key in that file // to support sharing the file with other subsystems. // // Credentials should be stored in the config file in a format that // matches their definitions in client.go func NewConfigFileClient(filesystem fs.FS, name, key string) (ClientManager, error) { fp, err := filesystem.Open(name) if err != nil { return nil, err } defer fp.Close() fc := map[string]any{} switch e := filepath.Ext(name); e { case ".yaml", ".yml": if err := yaml.NewDecoder(fp).Decode(&fc); err != nil { return nil, err } case ".json": if err := json.NewDecoder(fp).Decode(&fc); err != nil { return nil, err } default: return nil, fmt.Errorf("Config files with extension %s are not supported", e) } ck, ok := fc[key] if !ok { return nil, fmt.Errorf("Key %s does not exist in config file", key) } cfg := map[string]any{} if err := mapstructure.Decode(ck, &cfg); err != nil { return nil, fmt.Errorf("Config file key was not of correct type: %w", err) } return &ConfigFileClient{ c: make(chan Renewal), cfg: cfg, }, nil } func (c *ConfigFileClient) DatabaseCredential(ctx context.Context, path string) (*Credential, Handle, error) { out := Credential{} hnd, err := c.RawSecret(ctx, path, &out) if err != nil { return nil, nil, err } return &out, hnd, nil } func (c *ConfigFileClient) Secret(ctx context.Context, path string, out any) (Handle, error) { return c.RawSecret(ctx, path, out) } func (c *ConfigFileClient) RawSecret(ctx context.Context, path string, out any) (Handle, error) { v, ok := c.cfg[path] if !ok { return nil, fmt.Errorf("Secret %s was not found in config file", path) } if err := mapstructure.Decode(v, &out); err != nil { return nil, err } return &ConfigFileHandle{}, nil } func (c *ConfigFileClient) AWSIAMUser(ctx context.Context, name string) (*AWSCredential, Handle, error) { out := AWSCredential{} hnd, err := c.RawSecret(ctx, name, &out) if err != nil { return nil, nil, err } return &out, hnd, nil } func (c *ConfigFileClient) AWSAssumeRoleSimple(ctx context.Context, name string) (*AWSCredential, Handle, error) { return c.AWSAssumeRole(ctx, name, "", time.Second) } func (c *ConfigFileClient) AWSAssumeRole(ctx context.Context, name string, sessionName string, ttl time.Duration) (*AWSCredential, Handle, error) { return nil, nil, fmt.Errorf("Assuming AWS roles is not supported by ConfigFileClient") } func (c *ConfigFileClient) WriteSecret(ctx context.Context, path string, in any) error { return nil } func (c *ConfigFileClient) Encrypt(ctx context.Context, suffix string, data []byte) (string, error) { return "", nil } func (c *ConfigFileClient) Decrypt(ctx context.Context, suffix, data string) ([]byte, error) { return nil, nil } func (c *ConfigFileClient) Destroy(h Handle) error { return nil } func (c *ConfigFileClient) MakeNonCritical(h Handle) error { return nil } func (c *ConfigFileClient) Authenticate(ctx context.Context) error { return nil } func (c *ConfigFileClient) Notifications() <-chan Renewal { return c.c } func (c *ConfigFileClient) Run(ctx context.Context, wg *sync.WaitGroup) error { wg.Add(1) defer wg.Done() for { select { case <-ctx.Done(): close(c.c) return nil } } }