Skip to content

Commit

Permalink
feat!: Add store configuration support for SSM store
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 committed Jul 3, 2024
1 parent 1a4a704 commit a98febd
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

Check warning on line 19 in store/nullstore.go

View check run for this annotation

Codecov / codecov/patch

store/nullstore.go#L16-L19

Added lines #L16 - L19 were not covered by tests
}

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

Check warning on line 23 in store/nullstore.go

View check run for this annotation

Codecov / codecov/patch

store/nullstore.go#L22-L23

Added lines #L22 - L23 were not covered by tests
}

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

Check warning on line 78 in store/s3store.go

View check run for this annotation

Codecov / codecov/patch

store/s3store.go#L75-L78

Added lines #L75 - L78 were not covered by tests
}

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

Check warning on line 82 in store/s3store.go

View check run for this annotation

Codecov / codecov/patch

store/s3store.go#L81-L82

Added lines #L81 - L82 were not covered by tests
}

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")

Check warning on line 124 in store/secretsmanagerstore.go

View check run for this annotation

Codecov / codecov/patch

store/secretsmanagerstore.go#L123-L124

Added lines #L123 - L124 were not covered by tests
}

// 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

Check warning on line 116 in store/ssmstore.go

View check run for this annotation

Codecov / codecov/patch

store/ssmstore.go#L116

Added line #L116 was not covered by tests
}
}

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

Check warning on line 122 in store/ssmstore.go

View check run for this annotation

Codecov / codecov/patch

store/ssmstore.go#L122

Added line #L122 was not covered by tests
}
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)

Check warning on line 130 in store/ssmstore.go

View check run for this annotation

Codecov / codecov/patch

store/ssmstore.go#L130

Added line #L130 was not covered by tests
}

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

Check warning on line 135 in store/ssmstore.go

View check run for this annotation

Codecov / codecov/patch

store/ssmstore.go#L135

Added line #L135 was not covered by tests
}
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 a98febd

Please sign in to comment.