setec

package
v0.0.0-...-45231cc Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 1, 2024 License: BSD-3-Clause Imports: 23 Imported by: 4

Documentation

Overview

Package setec is a client library to access and manage secrets stored remotely in a secret management service.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache interface {
	// Write persists the given bytes for future retrieval.
	//
	// The data written to a Cache by the Store are a JSON object having the
	// following structure:
	//
	//   {
	//      "secret-name": {
	//         "secret": {"Value": <bytes>, "Version": <version>},
	//         "lastAccess": <last-access-time>,
	//      },
	//      ...
	//   }
	//
	// The "secret" field is an api.SecretValue for the latest known value
	// obtained from the service.  The "lastAccess" field is a Unix epoch
	// timestamp in seconds for the last time this secret name was requested
	// from the store, used to expire stale undeclared secrets.
	Write([]byte) error

	// Read returns previously persisted bytes, if any are available.  If the
	// cache is empty, Read must report an empty slice or nil without error.
	Read() ([]byte, error)
}

Cache is an interface that lets a Store persist one piece of data locally. The persistence need not be perfect (i.e., it's okay to lose previously written data).

type Client

type Client struct {
	// Server is the URL of the secrets server to talk to.
	Server string
	// DoHTTP is the function to use to make HTTP requests. If nil,
	// http.DefaultClient.Do is used.
	DoHTTP func(*http.Request) (*http.Response, error)
}

Client is a raw client to the secret management server. If you're just consuming secrets, you probably want to use a Store instead.

func (Client) Activate

func (c Client) Activate(ctx context.Context, name string, version api.SecretVersion) error

Activate changes the active version of the secret called name to version.

Access requirement: "activate"

func (Client) Delete

func (c Client) Delete(ctx context.Context, name string) error

Delete deletes all versions of the named secret.

Note: Delete will delete all versions of the secret, including the active one, if the caller has permission to do so.

Access requirement: "delete"

func (Client) DeleteVersion

func (c Client) DeleteVersion(ctx context.Context, name string, version api.SecretVersion) error

DeleteVersion deletes the specified version of the named secret.

Note: DeleteVersion will report an error if the caller attempts to delete the active version, even if they have permission to do so.

Access requirement: "delete"

func (Client) Get

func (c Client) Get(ctx context.Context, name string) (*api.SecretValue, error)

Get fetches the current active secret value for name.

Access requirement: "get"

func (Client) GetIfChanged

func (c Client) GetIfChanged(ctx context.Context, name string, oldVersion api.SecretVersion) (*api.SecretValue, error)

GetIfChanged fetches a secret value by name, if the active version on the server is different from oldVersion. If the active version on the server is the same as oldVersion, it reports api.ErrValueNotChanged without returning a secret. As a special case, if oldVersion == 0 then GetIfVersion behaves as Get and retrieves the current active version.

Access requirement: "get"

func (Client) GetVersion

func (c Client) GetVersion(ctx context.Context, name string, version api.SecretVersion) (*api.SecretValue, error)

Get fetches a secret value by name and version. If version == 0, GetVersion retrieves the current active version.

Access requirement: "get"

func (Client) Info

func (c Client) Info(ctx context.Context, name string) (*api.SecretInfo, error)

Info fetches metadata for a given secret name.

Access requirement: "info"

func (Client) List

func (c Client) List(ctx context.Context) ([]*api.SecretInfo, error)

List fetches a list of secret names and associated metadata for all those secrets on which the caller has "info" access. List does not report the secret values themselves. If the caller does not have "info" access to any secrets, List reports zero values without error.

func (Client) Put

func (c Client) Put(ctx context.Context, name string, value []byte) (version api.SecretVersion, err error)

Put creates a secret called name, with the given value. If a secret called name already exist, the value is saved as a new inactive version.

Access requirement: "put"

type Fields

type Fields struct {
	// contains filtered or unexported fields
}

Fields is a helper for plumbing secrets to the fields of struct values. The ParseFields function recognizes fields of a struct with a tag like:

setec:"base-secret-name[,json]"

The resulting Fields value fetches the secrets identified by these tags from a setec.Store, and injects their values into the fields.

Background

A program that uses multiple secret values has to plumb those secrets down to the code that needs them. One way to manage this is to bundle the secrets together into fields of a struct, and to make that struct accessible via a shared configuration library or through a context argument.

To simplify the manual process of adding fields and hooking them up to the secrets service, the Fields type uses reflection to discover the names of secrets declared via struct tags, and to handle the boilerplate of plumbing secret values to those fields.

Basic usage

Populate the Structs field of the StoreConfig with pointers to the struct values to be populated. You may optionally provide a prefix to prepend to secret names, so that it can use different secrets in different environments (for example dev vs. prod):

st, err := setec.NewStore(ctx, setec.StoreConfig{
   Client:  client,
   Structs: []setec.Struct{{Value: &v, Prefix: "dev/program-name"}},
})
// ...

Once the store is ready, the secret values are automatically copied to the corresponding fields. It is also possible to explicitly populate struct fields after the store is constructed, see ParseFields. The store must be constructed with the AllowLookup option enabled to add new secrets after the store has been constructed.

Field Types

The Fields type can handle struct fields of the following types:

  • A field of type []byte receives a copy of the secret value.

  • A field of type string receives a copy of the secret as a string.

  • A field of type setec.Secret is populated with a handle to the secret.

  • A field of type setec.Watcher is populated with a watcher for the secret.

  • A field whose (pointer) type implements the encoding.BinaryUnmarshaler interface has its UnmarshalBinary method called with the secret value. This may be used to handle structured data, or to add validation.

In addition, a field may have any type that supports JSON encoding, provided the secret value is also encoded as JSON, if its tag includes the optional "json" verb. For example, given:

type Key struct {
   Salt []byte `json:"iv"`
   Data []byte `json:"data"`
}

the following is a valid field declaration:

SecretKey Key `setec:"secret-key,json"`  // note "json" verb

and accepts a secret value formatted like:

{"iv":"aGVsbG8sIHdvcmxk","data":"c3VwZXIgc2VjcmV0IHNxdWlycmVsIHN0dWZm"}

The ParseFields function will report an error for a tagged field whose type does not fit within these constraints.

func ParseFields

func ParseFields(v any, namePrefix string) (*Fields, error)

ParseFields parses information about setec-tagged fields from v. The concrete type of v must be a pointer to a struct value with at least one tagged field. The namePrefix, if non-empty, is joined to the front of each tagged name, separated with a slash ("/").

See Fields for a description of the struct tags and types recognized.

func (*Fields) Apply

func (f *Fields) Apply(ctx context.Context, s *Store) error

Apply fetches and applies the secret values required by f to the corresponding fields of the input struct. Each secret must either be known to s at initialization, or s must be configured to allow lookups. Apply will attempt to process all tagged fields before reporting an error.

Note: When applying secrets to struct fields from an existing Store, the AllowLookup option of the Store must be enabled, or else Apply will report an error for any field that refers to a secret not already available.

func (*Fields) Secrets

func (f *Fields) Secrets() []string

Secrets returns the full prefix-expanded names of the secrets needed by fields tagged in f.

type FileCache

type FileCache string

FileCache is an implementation of the Cache interface that stores a value in a file at the specified path.

func NewFileCache

func NewFileCache(path string) (FileCache, error)

NewFileCache constructs a new file cache associated with the specified path. The cache file is not created, but an error is reported if the enclosing directory cannot be created, or if the path exists but is not a plain file.

func (FileCache) Read

func (f FileCache) Read() ([]byte, error)

func (FileCache) Write

func (f FileCache) Write(data []byte) error

type FileClient

type FileClient struct {
	// contains filtered or unexported fields
}

FileClient is an implementation of the StoreClient interface that vends secrets from a static collection of data stored locally on disk.

This is intended for use in bootstrapping and deployments without access to a separate secrets server.

func NewFileClient

func NewFileClient(path string) (*FileClient, error)

NewFileClient constructs a new FileClient using the contents of the specified local file path. The file must contain a JSON object having the following structure:

{
   "secret-name-1": {
      "secret": {"Value": "b3BlbiBzZXNhbWU=", "Version": 1}
   },
   "secret-name-2": {
      "secret": {"TextValue": "xyzzy", "Version": 5}
   },
   ...
}

The secret values are encoded either as base64 strings ("Value") or as plain text ("TextValue"). A cache file written out by a FileCache can also be used as input. Unlike a cache, however, a FileClient only reads the file once, and subsequent modifications of the file are not observed.

func (*FileClient) Get

func (fc *FileClient) Get(_ context.Context, name string) (*api.SecretValue, error)

Get implements the corresponding method of StoreClient.

func (*FileClient) GetIfChanged

func (fc *FileClient) GetIfChanged(_ context.Context, name string, oldVersion api.SecretVersion) (*api.SecretValue, error)

GetIfChanged implements the corresponding method of StoreClient.

type MemCache

type MemCache struct {
	// contains filtered or unexported fields
}

MemCache is a trivial implementation of the Cache interface that stores a value in a byte slice. This is intended for use in testing. The methods of a MemCache never report an error.

func NewMemCache

func NewMemCache(s string) *MemCache

NewMemCache constructs a new memory cache whose initial contents are s.

func (*MemCache) Read

func (m *MemCache) Read() ([]byte, error)

func (*MemCache) String

func (m *MemCache) String() string

func (*MemCache) Write

func (m *MemCache) Write(data []byte) error

type Secret

type Secret func() []byte

A Secret is a function that fetches the current active value of a secret. The caller should not cache the value returned; the function does not block and will always report a valid (if possibly stale) result.

The Secret retains ownership of the bytes returned, but the store will never modify the contents of the secret, so it is safe to share the slice without copying as long as the caller does not modify them.

func StaticFile

func StaticFile(path string) (Secret, error)

StaticFile returns a Secret that vends the contents of path. The contents of the file are returned exactly as stored.

This is useful as a placeholder for development, migration, and testing. The value reported by this secret is the contents of path at the time this function is called, and never changes.

func StaticSecret

func StaticSecret(value string) Secret

StaticSecret returns a Secret that vends a static string value. This is useful as a placeholder for development, migration, and testing. The value reported by a static secret never changes.

func StaticTextFile

func StaticTextFile(path string) (Secret, error)

StaticTextFile returns a secret that vends the contents of path, which are treated as text with leading and trailing whitespace trimmed.

This is useful as a placeholder for development, migration, and testing. The value reported by a static secret never changes.

func (Secret) Get

func (s Secret) Get() []byte

Get returns the current active value of the secret. It is a legibility alias for calling the function.

func (Secret) GetString

func (s Secret) GetString() string

GetString returns a copy of the current active value of the secret as a string.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is a store that provides named secrets.

func NewStore

func NewStore(ctx context.Context, cfg StoreConfig) (*Store, error)

NewStore creates a secret store with the given configuration. The service URL of the client must be set.

NewStore blocks until all the secrets named in cfg.Secrets are available for retrieval by the Secret method, or ctx ends. The context passed to NewStore is only used for initializing the store. If a cache is provided, cached values are accepted even if stale, as long as there is a value for each of the secrets in cfg.

func (*Store) Close

func (s *Store) Close() error

Close stops the background task polling for updates and waits for it to exit.

func (*Store) LookupSecret

func (s *Store) LookupSecret(ctx context.Context, name string) (Secret, error)

LookupSecret returns a fetcher for the named secret. If name is already known by s, this is equivalent to Secret; otherwise, s attempts to fetch the latest active version of the secret from the service and either adds it to the collection or reports an error. LookupSecret does not automatically retry in case of errors.

func (*Store) LookupWatcher

func (s *Store) LookupWatcher(ctx context.Context, name string) (Watcher, error)

LookupWatcher returns a watcher for the named secret. If name is already known by s, this is equivalent to Watcher; otherwise s attempts to fetch the latest active version of the secret from the service and either adds it to the collection or reports an error. LookupWatcher does not automatically retry in case of errors.

func (*Store) Metrics

func (s *Store) Metrics() *expvar.Map

Metrics returns a map of metrics for s. The caller is responsible for publishing the map to the metrics exporter.

func (*Store) Refresh

func (s *Store) Refresh(ctx context.Context) error

Refresh synchronously checks for new versions of all the secrets currently known by s. It blocks until the refresh is complete or until ctx ends.

Updates are managed automatically when a Store is created and by the polling mechanism, but a caller may invoke Refresh directly if it wants to check for new secret values at a specific moment.

func (*Store) Secret

func (s *Store) Secret(name string) Secret

Secret returns a fetcher for the named secret.

If s has lookups enabled, Secret returns nil for an unknown name. Otherwise, Secret panics for an unknown name.

func (*Store) Watcher

func (s *Store) Watcher(name string) Watcher

Watcher returns a watcher for the named secret.

If s has lookups enabled, Watcher returns a zero Watcher for an unknown name. Otherwise, Watcher panics for an unknown name.

type StoreClient

type StoreClient interface {
	// Get fetches the current active secret value for name. See [Client.Get].
	Get(ctx context.Context, name string) (*api.SecretValue, error)

	// GetIfChanged fetches a secret value by name, if the active version on the
	// server is different from oldVersion. See [Client.GetIfChanged].
	GetIfChanged(ctx context.Context, name string, oldVersion api.SecretVersion) (*api.SecretValue, error)
}

StoreClient is the interface to the setec API used by the Store.

type StoreConfig

type StoreConfig struct {
	// Client is the API client used to fetch secrets from the service.
	// The service URL must be non-empty.
	Client StoreClient

	// Secrets are the names of secrets this Store should retrieve.
	//
	// Unless AllowLookup is true, only secrets named here or in the Structs
	// field can be read out of the store and an error is reported if no secrets
	// are listed.
	Secrets []string

	// Structs are optional struct values with tagged fields that should be
	// populated with secrets from the store at initialization time.
	//
	// Unless AllowLookup is true, only secrets named here or in the Secrets
	// field can be read out of the store and an error is reported if no secrets
	// are listed.
	Structs []Struct

	// AllowLookup instructs the store to allow the caller to look up secrets
	// not known to the store at the time of construction. If false, only
	// secrets pre-declared in the Secrets and Structs slices can be fetched,
	// and the Lookup and LookupWatcher methods will report an error for all
	// un-listed secrets.
	AllowLookup bool

	// Cache, if non-nil, is a cache that persists secrets locally.
	//
	// Depending on the implementation, local caching may degrade security
	// slightly by making secrets easier to get at, but in return allows the
	// Store to initialize and run during outages of the secrets management
	// service.
	//
	// If no cache is provided, the Store caches secrets in-memory for the
	// lifetime of the process only.
	Cache Cache

	// PollInterval is the interval at which the store will poll the service for
	// updated secret values. If zero, a default value is used. If negative, the
	// store does not automatically poll and the caller must explicitly call the
	// Refresh method to effect an update.
	//
	// This field is ignored if PollTicker is set.
	PollInterval time.Duration

	// ExpiryAge is a duration beyond which undeclared secrets that have not
	// been accessed in that time are eligible for expiration from the cache.
	// A zero value means secrets do not expire.
	ExpiryAge time.Duration

	// Logf is a logging function where text logs should be sent.  If nil, logs
	// are written to the standard log package.
	Logf logger.Logf

	// PollTicker, if set is a ticker that is used to control the scheduling of
	// update polls. If nil, a time.Ticker is used based on the PollInterval.
	PollTicker Ticker

	// TimeNow, if set, is a function that reports a Time to be treated as the
	// current wallclock time.  If nil, time.Now is used.
	TimeNow func() time.Time
}

StoreConfig is the configuration for Store.

type Struct

type Struct struct {
	// Value must be a non-nil pointer to a value of struct type, having at
	// least one field tagged with a "setec" field tag.
	// See Fields for a description of the tag format.
	Value any

	// Prefix is an optional prefix that should be prepended to each secret
	// described by a tag in Value to obtain the secret name to look up.
	Prefix string
}

Struct describes a struct value with tagged fields that should be populated with secrets from a Store.

type Ticker

type Ticker interface {
	// Chan returns a channel upon which time values are delivered to signal
	// that a poll is required.
	Chan() <-chan time.Time

	// Stop signals that the ticker should stop and deliver no more values.
	Stop()

	// Done is invoked when a signaled poll is complete.
	Done()
}

A Ticker is used to inject time control in to the polling loop of a store.

type Updater

type Updater[T any] struct {
	// contains filtered or unexported fields
}

An Updater tracks a value whose state depends on a secret, together with a watcher for updates to the secret. The caller provides a function to update the value when a new version of the secret is delivered, and the Updater manages access and updates to the value.

func NewUpdater

func NewUpdater[T any](w Watcher, newValue func([]byte) T) *Updater[T]

NewUpdater creates a new Updater that tracks updates to a value based on new secret versions delivered to w. The newValue function returns a new value of the type based on its argument, a secret value.

The initial value is constructed by calling newValue with the current secret version in w at the time NewUpdater is called. Calls to the Get method update the value as needed when w changes.

The updater synchronizes calls to Get and newValue, so the callback can safely interact with shared state without additional locking.

func (*Updater[T]) Get

func (u *Updater[T]) Get() T

Get fetches the current value of u, first updating it if the secret has changed. It is safe to call Get concurrently from multiple goroutines.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

A Watcher monitors the current active value of a secret, and allows the user to be notified when the value of the secret changes.

func (Watcher) Get

func (w Watcher) Get() []byte

Get returns the current active value of the secret.

func (Watcher) IsValid

func (w Watcher) IsValid() bool

IsValid reports whether w is valid, meaning that it has a secret available.

func (Watcher) Ready

func (w Watcher) Ready() <-chan struct{}

Ready returns a channel that delivers a value when the current active version of the secret has changed. The channel is never closed.

The ready channel is a level trigger. The Watcher does not queue multiple notifications, and if the caller does not drain the channel subsequent notifications will be dropped.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL