Skip to content

Commit

Permalink
feat!: Add store configuration support for SSM store (#532)
Browse files Browse the repository at this point in the history
The new Config and SetConfig methods on the Store interface allow
implementations to maintain their own configurations. Only the SSM store
fully implements the methods; the others return an empty configuration
and do not support setting one.

The store configuration itself holds a list of required tags for each
written secret. Enforcement of the tags is not yet implemented. Although
the configuration struct is exported from its package, it is not part of
chamber's client interface and is subject to change at any time.

The SSM store implementation stores its configuration as a secret
inside the newly reserved "_chamber" service as a JSON document. The
schema is not exported, so users shouldn't build anything from it.
  • Loading branch information
bhavanki authored Aug 1, 2024
1 parent 1a4a704 commit dd1ae9a
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 0 deletions.
10 changes: 10 additions & 0 deletions store/nullstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ func NewNullStore() *NullStore {
return &NullStore{}
}

func (s *NullStore) Config(ctx context.Context) (StoreConfig, error) {
return StoreConfig{
Version: LatestStoreConfigVersion,
}, nil
}

func (s *NullStore) SetConfig(ctx context.Context, config StoreConfig) error {
return errors.New("SetConfig is not implemented for Null Store")
}

func (s *NullStore) Write(ctx context.Context, id SecretId, value string) error {
return errors.New("Write is not implemented for Null Store")
}
Expand Down
10 changes: 10 additions & 0 deletions store/s3store.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ func NewS3StoreWithBucket(ctx context.Context, numRetries int, bucket string) (*
}, nil
}

func (s *S3Store) Config(ctx context.Context) (StoreConfig, error) {
return StoreConfig{
Version: LatestStoreConfigVersion,
}, nil
}

func (s *S3Store) SetConfig(ctx context.Context, config StoreConfig) error {
return errors.New("Not implemented for S3 Store")
}

func (s *S3Store) Write(ctx context.Context, id SecretId, value string) error {
index, err := s.readLatest(ctx, id.Service)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions store/secretsmanagerstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ func NewSecretsManagerStore(ctx context.Context, numRetries int) (*SecretsManage
}, nil
}

func (s *SecretsManagerStore) Config(ctx context.Context) (StoreConfig, error) {
return StoreConfig{
Version: LatestStoreConfigVersion,
}, nil
}

func (s *SecretsManagerStore) SetConfig(ctx context.Context, config StoreConfig) error {
return errors.New("Not implemented for Secrets Manager Store")
}

// Write writes a given value to a secret identified by id. If the secret
// already exists, then write a new version.
func (s *SecretsManagerStore) Write(ctx context.Context, id SecretId, value string) error {
Expand Down
10 changes: 10 additions & 0 deletions store/secretsmanagerstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,13 @@ func uniqueID() string {
_, _ = rand.Read(uuid)
return fmt.Sprintf("%x", uuid)
}

func TestSecretsManagerStoreConfig(t *testing.T) {
store := &SecretsManagerStore{}

config, err := store.Config(context.Background())

assert.NoError(t, err)
assert.Equal(t, LatestStoreConfigVersion, config.Version)
assert.Empty(t, config.RequiredTags)
}
44 changes: 44 additions & 0 deletions store/ssmstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package store

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -93,6 +94,49 @@ func (s *SSMStore) KMSKey() string {
return fromEnv
}

const (
storeConfigKey = "store-config"
)

var (
storeConfigID = SecretId{
Service: ChamberService,
Key: storeConfigKey,
}
)

func (s *SSMStore) Config(ctx context.Context) (StoreConfig, error) {
configSecret, err := s.readLatest(ctx, storeConfigID)
if err != nil {
if err == ErrSecretNotFound {
return StoreConfig{
Version: LatestStoreConfigVersion,
}, nil
} else {
return StoreConfig{}, err
}
}

var config StoreConfig
if err := json.Unmarshal([]byte(*configSecret.Value), &config); err != nil {
return StoreConfig{}, fmt.Errorf("failed to unmarshal store config: %w", err)
}
return config, nil
}

func (s *SSMStore) SetConfig(ctx context.Context, config StoreConfig) error {
configBytes, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal store config: %w", err)
}

err = s.write(ctx, storeConfigID, string(configBytes), nil)
if err != nil {
return fmt.Errorf("failed to write store config: %w", err)
}
return nil
}

// Write writes a given value to a secret identified by id. If the secret
// already exists, then write a new version.
func (s *SSMStore) Write(ctx context.Context, id SecretId, value string) error {
Expand Down
57 changes: 57 additions & 0 deletions store/ssmstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package store
import (
"context"
"errors"
"fmt"
"os"
"sort"
"strings"
Expand Down Expand Up @@ -853,3 +854,59 @@ type ByKeyRaw []RawSecret
func (a ByKeyRaw) Len() int { return len(a) }
func (a ByKeyRaw) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKeyRaw) Less(i, j int) bool { return a[i].Key < a[j].Key }

func TestSSMStoreConfig(t *testing.T) {
storeConfigName := fmt.Sprintf("/%s/%s", ChamberService, storeConfigKey)
parameters := map[string]mockParameter{
storeConfigName: {
currentParam: &types.Parameter{
Name: aws.String(storeConfigName),
Type: types.ParameterTypeSecureString,
Value: aws.String(`{"version":"2","requiredTags":["key1", "key2"]}`),
},
meta: &types.ParameterMetadata{
Name: aws.String(storeConfigName),
Description: aws.String("1"),
LastModifiedDate: aws.Time(time.Now()),
LastModifiedUser: aws.String("test"),
},
},
}
store := NewTestSSMStore(parameters)

config, err := store.Config(context.Background())

assert.NoError(t, err)
assert.Equal(t, "2", config.Version)
assert.Equal(t, []string{"key1", "key2"}, config.RequiredTags)
}

func TestSSMStoreConfig_Missing(t *testing.T) {
parameters := map[string]mockParameter{}
store := NewTestSSMStore(parameters)

config, err := store.Config(context.Background())

assert.NoError(t, err)
assert.Equal(t, LatestStoreConfigVersion, config.Version)
assert.Empty(t, config.RequiredTags)
}

func TestSSMStoreSetConfig(t *testing.T) {
parameters := map[string]mockParameter{}
store := NewTestSSMStore(parameters)

config := StoreConfig{
Version: "2.1",
RequiredTags: []string{"key1.1", "key2.1"},
}
err := store.SetConfig(context.Background(), config)

assert.NoError(t, err)

config, err = store.Config(context.Background())

assert.NoError(t, err)
assert.Equal(t, "2.1", config.Version)
assert.Equal(t, []string{"key1.1", "key2.1"}, config.RequiredTags)
}
14 changes: 14 additions & 0 deletions store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ func ReservedService(service string) bool {
return service == ChamberService
}

const (
LatestStoreConfigVersion = "1"
)

// StoreConfig holds configuration information for a store. WARNING: Despite
// its public visibility, the contents of this struct are subject to change at
// any time, and are not part of the public interface for chamber.
type StoreConfig struct {
Version string `json:"version"`
RequiredTags []string `json:"requiredTags,omitempty"`
}

type ChangeEventType int

const (
Expand Down Expand Up @@ -73,6 +85,8 @@ type ChangeEvent struct {

// Store is an interface for a secret store.
type Store interface {
Config(ctx context.Context) (StoreConfig, error)
SetConfig(ctx context.Context, config StoreConfig) error
Write(ctx context.Context, id SecretId, value string) error
WriteWithTags(ctx context.Context, id SecretId, value string, tags map[string]string) error
Read(ctx context.Context, id SecretId, version int) (Secret, error)
Expand Down

0 comments on commit dd1ae9a

Please sign in to comment.