open-vault/vendor/github.com/hashicorp/vault-plugin-secrets-gcpkms/backend.go
2019-04-15 14:59:52 -04:00

212 lines
5.9 KiB
Go

package gcpkms
import (
"context"
"sync"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/useragent"
"github.com/hashicorp/vault/sdk/logical"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
kmsapi "cloud.google.com/go/kms/apiv1"
)
var (
// defaultClientLifetime is the amount of time to cache the KMS client. This
// has to be less than 60 minutes or the oauth token will expire and
// subsequent requests will fail. The reason we cache the client is because
// the process for looking up credentials is not performant and the overhead
// is too significant for a plugin that will receive this much traffic.
defaultClientLifetime = 30 * time.Minute
)
type backend struct {
*framework.Backend
// kmsClient is the actual client for connecting to KMS. It is cached on
// the backend for efficiency.
kmsClient *kmsapi.KeyManagementClient
kmsClientCreateTime time.Time
kmsClientLifetime time.Duration
kmsClientLock sync.RWMutex
// ctx and ctxCancel are used to control overall plugin shutdown. These
// contexts are given to any client libraries or requests that should be
// terminated during plugin termination.
ctx context.Context
ctxCancel context.CancelFunc
ctxLock sync.Mutex
}
// Factory returns a configured instance of the backend.
func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
if err := b.Setup(ctx, c); err != nil {
return nil, err
}
return b, nil
}
// Backend returns a configured instance of the backend.
func Backend() *backend {
var b backend
b.kmsClientLifetime = defaultClientLifetime
b.ctx, b.ctxCancel = context.WithCancel(context.Background())
b.Backend = &framework.Backend{
BackendType: logical.TypeLogical,
Help: "The GCP KMS secrets engine provides pass-through encryption and " +
"decryption to Google Cloud KMS keys.",
Paths: []*framework.Path{
b.pathConfig(),
b.pathKeys(),
b.pathKeysCRUD(),
b.pathKeysConfigCRUD(),
b.pathKeysDeregister(),
b.pathKeysRegister(),
b.pathKeysRotate(),
b.pathKeysTrim(),
b.pathDecrypt(),
b.pathEncrypt(),
b.pathPubkey(),
b.pathReencrypt(),
b.pathSign(),
b.pathVerify(),
},
Invalidate: b.invalidate,
Clean: b.clean,
}
return &b
}
// clean cancels the shared contexts. This is called just before unmounting
// the plugin.
func (b *backend) clean(_ context.Context) {
b.ctxLock.Lock()
b.ctxCancel()
b.ctxLock.Unlock()
}
// invalidate resets the plugin. This is called when a key is updated via
// replication.
func (b *backend) invalidate(ctx context.Context, key string) {
switch key {
case "config":
b.ResetClient()
}
}
// ResetClient closes any connected clients.
func (b *backend) ResetClient() {
b.kmsClientLock.Lock()
b.resetClient()
b.kmsClientLock.Unlock()
}
// resetClient rests the underlying client. The caller is responsible for
// acquiring and releasing locks. This method is not safe to call concurrently.
func (b *backend) resetClient() {
if b.kmsClient != nil {
b.kmsClient.Close()
b.kmsClient = nil
}
b.kmsClientCreateTime = time.Unix(0, 0).UTC()
}
// KMSClient creates a new client for talking to the GCP KMS service.
func (b *backend) KMSClient(s logical.Storage) (*kmsapi.KeyManagementClient, func(), error) {
// If the client already exists and is valid, return it
b.kmsClientLock.RLock()
if b.kmsClient != nil && time.Now().UTC().Sub(b.kmsClientCreateTime) < b.kmsClientLifetime {
closer := func() { b.kmsClientLock.RUnlock() }
return b.kmsClient, closer, nil
}
b.kmsClientLock.RUnlock()
// Acquire a full lock. Since all invocations acquire a read lock and defer
// the release of that lock, this will block until all clients are no longer
// in use. At that point, we can acquire a globally exclusive lock to close
// any connections and create a new client.
b.kmsClientLock.Lock()
b.Logger().Debug("creating new KMS client")
// Attempt to close an existing client if we have one.
b.resetClient()
// Get the config
config, err := b.Config(b.ctx, s)
if err != nil {
b.kmsClientLock.Unlock()
return nil, nil, err
}
// If credentials were provided, use those. Otherwise fall back to the
// default application credentials.
var creds *google.Credentials
if config.Credentials != "" {
creds, err = google.CredentialsFromJSON(b.ctx, []byte(config.Credentials), config.Scopes...)
if err != nil {
b.kmsClientLock.Unlock()
return nil, nil, errwrap.Wrapf("failed to parse credentials: {{err}}", err)
}
} else {
creds, err = google.FindDefaultCredentials(b.ctx, config.Scopes...)
if err != nil {
b.kmsClientLock.Unlock()
return nil, nil, errwrap.Wrapf("failed to get default token source: {{err}}", err)
}
}
// Create and return the KMS client with a custom user agent.
client, err := kmsapi.NewKeyManagementClient(b.ctx,
option.WithCredentials(creds),
option.WithScopes(config.Scopes...),
option.WithUserAgent(useragent.String()),
)
if err != nil {
b.kmsClientLock.Unlock()
return nil, nil, errwrap.Wrapf("failed to create KMS client: {{err}}", err)
}
// Cache the client
b.kmsClient = client
b.kmsClientCreateTime = time.Now().UTC()
b.kmsClientLock.Unlock()
b.kmsClientLock.RLock()
closer := func() { b.kmsClientLock.RUnlock() }
return client, closer, nil
}
// Config parses and returns the configuration data from the storage backend.
// Even when no user-defined data exists in storage, a Config is returned with
// the default values.
func (b *backend) Config(ctx context.Context, s logical.Storage) (*Config, error) {
c := DefaultConfig()
entry, err := s.Get(ctx, "config")
if err != nil {
return nil, errwrap.Wrapf("failed to get configuration from storage: {{err}}", err)
}
if entry == nil || len(entry.Value) == 0 {
return c, nil
}
if err := entry.DecodeJSON(&c); err != nil {
return nil, errwrap.Wrapf("failed to decode configuration: {{err}}", err)
}
return c, nil
}