diff --git a/internal/testing/setup/setup.go b/internal/testing/setup/setup.go index 2a7dd8f9b1..bbc73877f7 100644 --- a/internal/testing/setup/setup.go +++ b/internal/testing/setup/setup.go @@ -300,4 +300,4 @@ func FakeGCPDefaultCredentials(t *testing.T) func() { os.Remove(f.Name()) os.Setenv(envVar, oldEnvVal) } -} \ No newline at end of file +} diff --git a/secrets/awskms/example_test.go b/secrets/awskms/example_test.go index ddb55210b7..7de1b8cea7 100644 --- a/secrets/awskms/example_test.go +++ b/secrets/awskms/example_test.go @@ -19,6 +19,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws/session" + "gocloud.dev/secrets" "gocloud.dev/secrets/awskms" ) @@ -39,7 +40,7 @@ func Example() { // Construct a *secrets.Keeper. keeper := awskms.NewKeeper( client, - // Get the key resource ID. Here is an example of using an alias. See + // Get the key ID. Here is an example of using an alias. See // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn // for more details. "alias/test-secrets", @@ -56,3 +57,14 @@ func Example() { decrypted, err := keeper.Decrypt(ctx, ciphertext) _ = decrypted } + +func Example_openKeeper() { + ctx := context.Background() + + // OpenKeeper creates a *secrets.Keeper from a URL. + // The host + path are the key ID; this example uses an alias. See + // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn + // for more details. + k, err := secrets.OpenKeeper(ctx, "awskms://alias/my-key") + _, _ = k, err +} diff --git a/secrets/awskms/kms.go b/secrets/awskms/kms.go index 78ea82ae0d..4642466172 100644 --- a/secrets/awskms/kms.go +++ b/secrets/awskms/kms.go @@ -15,6 +15,16 @@ // Package awskms provides a secrets implementation backed by AWS KMS. // Use NewKeeper to construct a *secrets.Keeper. // +// URLs +// +// For secrets.OpenKeeper URLs, awskms registers for the scheme "awskms". +// The host+path are used as the key ID; see +// https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn +// for more details. Example: "awskms://alias/my-key". +// +// secrets.OpenKeeper will create a new AWS session with the default options. +// If you want to use a different session, see URLOpener. +// // As // // awskms exposes the following type for As: @@ -24,19 +34,98 @@ package awskms // import "gocloud.dev/secrets/awskms" import ( "context" "errors" + "fmt" + "net/url" + "path" + "sync" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/client" + "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" + gcaws "gocloud.dev/aws" "gocloud.dev/gcerrors" "gocloud.dev/internal/gcerr" "gocloud.dev/secrets" ) +func init() { + secrets.DefaultURLMux().RegisterKeeper(Scheme, new(lazySessionOpener)) +} + +// Dial gets an AWS KMS service client. +func Dial(p client.ConfigProvider) (*kms.KMS, error) { + if p == nil { + return nil, errors.New("getting KMS service: no AWS session provided") + } + return kms.New(p), nil +} + +// URLOpener opens secrets.Keeper URLs for AWS KMS, like "awskms://keyID". +// The key ID can be in the form of an Amazon Resource Name (ARN), alias +// name, or alias ARN. See +// https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn +// for more details. +// The URL Host + Path are used as the key ID, to support alias names like "awskms://alias/foo". +// See gocloud.dev/aws/ConfigFromURLParams for supported query parameters +// for modifying the aws.Session. +type URLOpener struct { + // ConfigProvider must be set to a non-nil value. + ConfigProvider client.ConfigProvider + + // Options specifies the options to pass to NewKeeper. + Options KeeperOptions +} + +// lazySessionOpener obtains the AWS session from the environment on the first +// call to OpenKeeperURL. +type lazySessionOpener struct { + init sync.Once + opener *URLOpener + err error +} + +func (o *lazySessionOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + o.init.Do(func() { + sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable}) + if err != nil { + o.err = err + return + } + o.opener = &URLOpener{ + ConfigProvider: sess, + } + }) + if o.err != nil { + return nil, fmt.Errorf("open AWS KMS Keeper %q: %v", u, o.err) + } + return o.opener.OpenKeeperURL(ctx, u) +} + +// Scheme is the URL scheme awskms registers its URLOpener under on secrets.DefaultMux. +const Scheme = "awskms" + +// OpenKeeperURL opens an AWS KMS Keeper based on u. +func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + configProvider := &gcaws.ConfigOverrider{ + Base: o.ConfigProvider, + } + overrideCfg, err := gcaws.ConfigFromURLParams(u.Query()) + if err != nil { + return nil, fmt.Errorf("open keeper %v: %v", u, err) + } + configProvider.Configs = append(configProvider.Configs, overrideCfg) + client, err := Dial(configProvider) + if err != nil { + return nil, err + } + return NewKeeper(client, path.Join(u.Host, u.Path), &o.Options), nil +} + // NewKeeper returns a *secrets.Keeper that uses AWS KMS. -// The keyID can be in the form of an Amazon Resource Name (ARN), alias +// The key ID can be in the form of an Amazon Resource Name (ARN), alias // name, or alias ARN. See // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn // for more details. @@ -48,14 +137,6 @@ func NewKeeper(client *kms.KMS, keyID string, opts *KeeperOptions) *secrets.Keep }) } -// Dial gets a AWS KMS service client. -func Dial(p client.ConfigProvider) (*kms.KMS, error) { - if p == nil { - return nil, errors.New("getting KMS service: no AWS session provided") - } - return kms.New(p), nil -} - type keeper struct { keyID string client *kms.KMS diff --git a/secrets/awskms/kms_test.go b/secrets/awskms/kms_test.go index 63e1e21320..fbd381ea56 100644 --- a/secrets/awskms/kms_test.go +++ b/secrets/awskms/kms_test.go @@ -113,3 +113,22 @@ func TestNoConnectionError(t *testing.T) { t.Error("got nil, want UnrecognizedClientException") } } + +func TestOpenKeeper(t *testing.T) { + tests := []struct { + URL string + WantErr bool + }{ + {"awskms://alias/my-key", false}, + {"awskms://alias/my-key?region=us-west1", false}, + {"awskms://alias/my-key?someparam=foo", true}, + } + + ctx := context.Background() + for _, test := range tests { + _, err := secrets.OpenKeeper(ctx, test.URL) + if (err != nil) != test.WantErr { + t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) + } + } +} diff --git a/secrets/gcpkms/example_test.go b/secrets/gcpkms/example_test.go index 481aa9c7f6..2564bedf24 100644 --- a/secrets/gcpkms/example_test.go +++ b/secrets/gcpkms/example_test.go @@ -18,6 +18,7 @@ import ( "context" "log" + "gocloud.dev/secrets" "gocloud.dev/secrets/gcpkms" ) @@ -52,3 +53,14 @@ func Example() { decrypted, err := keeper.Decrypt(ctx, ciphertext) _ = decrypted } + +func Example_openKeeper() { + ctx := context.Background() + + // OpenKeeper creates a *secrets.Keeper from a URL. + // The host + path are the key resourceID; see + // https://cloud.google.com/kms/docs/object-hierarchy#key + // for more information. + k, err := secrets.OpenKeeper(ctx, "gcpkms://projects/MYPROJECT/locations/MYLOCATION/keyRings/MYKEYRING/cryptoKeys/MYKEY") + _, _ = k, err +} diff --git a/secrets/gcpkms/kms.go b/secrets/gcpkms/kms.go index 83b5f0050f..3b00972be8 100644 --- a/secrets/gcpkms/kms.go +++ b/secrets/gcpkms/kms.go @@ -15,6 +15,17 @@ // Package gcpkms provides a secrets implementation backed by Google Cloud KMS. // Use NewKeeper to construct a *secrets.Keeper. // +// URLs +// +// For secrets.OpenKeeper URLs, gcpkms registers for the scheme "gcpkms". +// The host+path are used as the key resource ID; see +// https://cloud.google.com/kms/docs/object-hierarchy#key for more details. +// Example: "gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]". +// +// secrets.OpenKeeper will use Application Default Credentials, as described in +// https://cloud.google.com/docs/authentication/production. +// If you want to use different credentials, see URLOpener. +// // As // // gcpkms exposes the following type for As: @@ -24,6 +35,9 @@ package gcpkms // import "gocloud.dev/secrets/gcpkms" import ( "context" "fmt" + "net/url" + "path" + "sync" cloudkms "cloud.google.com/go/kms/apiv1" "gocloud.dev/gcerrors" @@ -46,9 +60,63 @@ func Dial(ctx context.Context, ts gcp.TokenSource) (*cloudkms.KeyManagementClien return c, func() { c.Close() }, err } +func init() { + secrets.DefaultURLMux().RegisterKeeper(Scheme, new(lazyCredsOpener)) +} + +// lazyCredsOpener obtains Application Default Credentials on the first call +// to OpenKeeperURL. +type lazyCredsOpener struct { + init sync.Once + opener *URLOpener + err error +} + +func (o *lazyCredsOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + o.init.Do(func() { + creds, err := gcp.DefaultCredentials(ctx) + if err != nil { + o.err = err + return + } + client, _, err := Dial(ctx, creds.TokenSource) + if err != nil { + o.err = err + return + } + o.opener = &URLOpener{Client: client} + }) + if o.err != nil { + return nil, fmt.Errorf("open GCP KMS %q: %v", u, o.err) + } + return o.opener.OpenKeeperURL(ctx, u) +} + +// Scheme is the URL scheme gcpkms registers its URLOpener under on secrets.DefaultMux. +const Scheme = "gcpkms" + +// URLOpener opens GCP KMS URLs like "gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]". +// The URL host+path are used as the key resource ID; see +// https://cloud.gogle.com/kms/docs/object-hierarchy#key for more details. +// No query parameters are accepted. +type URLOpener struct { + // Client must be non-nil and be authenticated with "cloudkms" scope or equivalent. + Client *cloudkms.KeyManagementClient + + // Options specifies the default options to pass to NewKeeper. + Options KeeperOptions +} + +// OpenKeeperURL opens the GCP KMS URLs. +func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + for param := range u.Query() { + return nil, fmt.Errorf("open keeper %q: invalid query parameter %q", u, param) + } + return NewKeeper(o.Client, path.Join(u.Host, u.Path), &o.Options), nil +} + // NewKeeper returns a *secrets.Keeper that uses Google Cloud KMS. -// See https://cloud.google.com/kms/docs/object-hierarchy#key for more -// information about the keyID format. +// See https://cloud.google.com/kms/docs/object-hierarchy#key for more details. // See the package documentation for an example. func NewKeeper(client *cloudkms.KeyManagementClient, keyID string, opts *KeeperOptions) *secrets.Keeper { return secrets.NewKeeper(&keeper{ @@ -58,14 +126,13 @@ func NewKeeper(client *cloudkms.KeyManagementClient, keyID string, opts *KeeperO } // KeyResourceID constructs a key resourceID for GCP KMS. -// See https://cloud.google.com/kms/docs/object-hierarchy#key for more -// information. +// See https://cloud.google.com/kms/docs/object-hierarchy#key for more details. func KeyResourceID(projectID, location, keyRing, key string) string { return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", projectID, location, keyRing, key) } -// keeper contains information to construct the pull path of a key. +// keeper implements driver.Keeper. type keeper struct { keyID string client *cloudkms.KeyManagementClient diff --git a/secrets/gcpkms/kms_test.go b/secrets/gcpkms/kms_test.go index 7af1fe5df9..421d864b99 100644 --- a/secrets/gcpkms/kms_test.go +++ b/secrets/gcpkms/kms_test.go @@ -102,3 +102,24 @@ func TestNoConnectionError(t *testing.T) { t.Error("got nil, want rpc error") } } + +func TestOpenKeeper(t *testing.T) { + cleanup := setup.FakeGCPDefaultCredentials(t) + defer cleanup() + + tests := []struct { + URL string + WantErr bool + }{ + {"gcpkms://projects/MYPROJECT/locations/MYLOCATION/keyRings/MYKEYRING/cryptoKeys/MYKEY", false}, + {"gcpkms://projects/MYPROJECT/locations/MYLOCATION/keyRings/MYKEYRING/cryptoKeys/MYKEY?param=val", true}, + } + + ctx := context.Background() + for _, test := range tests { + _, err := secrets.OpenKeeper(ctx, test.URL) + if (err != nil) != test.WantErr { + t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) + } + } +} diff --git a/secrets/localsecrets/example_test.go b/secrets/localsecrets/example_test.go index c605e2b711..4c2c281d2b 100644 --- a/secrets/localsecrets/example_test.go +++ b/secrets/localsecrets/example_test.go @@ -19,6 +19,7 @@ import ( "fmt" "log" + "gocloud.dev/secrets" "gocloud.dev/secrets/localsecrets" ) @@ -44,3 +45,17 @@ func Example() { // Output: // Hello, Secrets! } + +func Example_openKeeper() { + ctx := context.Background() + + // OpenKeeper creates a *secrets.Keeper from a URL. + + // Using "stringkey://", the first 32 bytes of the URL hostname is used as the secret. + k, err := secrets.OpenKeeper(ctx, "stringkey://my-secret-key") + + // Using "base64key://", the URL hostname must be a base64-encoded key. + // The first 32 bytes of the decoding are used as the secret key. + k, err = secrets.OpenKeeper(ctx, "base64key://bXktc2VjcmV0LWtleQ==") + _, _ = k, err +} diff --git a/secrets/localsecrets/localsecrets.go b/secrets/localsecrets/localsecrets.go index 813171da32..41c882597d 100644 --- a/secrets/localsecrets/localsecrets.go +++ b/secrets/localsecrets/localsecrets.go @@ -16,6 +16,14 @@ // locally provided symmetric key. // Use NewKeeper to construct a *secrets.Keeper. // +// URLs +// +// For secrets.OpenKeeper URLs, localsecrets registers for the schemes "stringkey" +// and "base64key". For "stringkey" (e.g., "stringkey://my-secret-key"), the +// first 32 bytes of the URL host are used as the secret key. For "base64key" +// (e.g., "base64key://bXktc2VjcmV0LWtleQ=="), the URL host is base64-decoded +// and the first 32 bytes of the result are used as the secret key. +// // As // // localsecrets does not support any types for As. @@ -24,14 +32,58 @@ package localsecrets // import "gocloud.dev/secrets/localsecrets" import ( "context" "crypto/rand" + "encoding/base64" "errors" + "fmt" "io" + "net/url" "gocloud.dev/gcerrors" "gocloud.dev/secrets" "golang.org/x/crypto/nacl/secretbox" ) +func init() { + secrets.DefaultURLMux().RegisterKeeper(SchemeString, &URLOpener{}) + secrets.DefaultURLMux().RegisterKeeper(SchemeBase64, &URLOpener{base64: true}) +} + +// SchemeString/SchemeBase64 are the URL schemes localsecrets registers its URLOpener under on secrets.DefaultMux. +// See the package documentation and/or URLOpener for details. +const ( + SchemeString = "stringkey" + SchemeBase64 = "base64key" +) + +// URLOpener opens localsecrets URLs like "stringkey://mykey" and "base64key://c2VjcmV0IGtleQ==". +// +// For stringkey, the first 32 bytes of the URL host are used as the symmetric +// key for encryption/decryption. +// +// For base64key, the URL host must be a base64 encoded string; the first 32 +// bytes of the decoded bytes are used as the symmetric key for +// encryption/decryption. +// +// No query parameters are accepted. +type URLOpener struct { + base64 bool +} + +// OpenKeeperURL opens Keeper URLs. +func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + for param := range u.Query() { + return nil, fmt.Errorf("open keeper %q: invalid query parameter %q", u, param) + } + if o.base64 { + sk, err := Base64Key(u.Host) + if err != nil { + return nil, fmt.Errorf("open keeper %q: base64 decode failed: %v", u, err) + } + return NewKeeper(sk), nil + } + return NewKeeper(ByteKey(u.Host)), nil +} + // keeper holds a secret for use in symmetric encryption, // and implements driver.Keeper. type keeper struct { @@ -46,6 +98,18 @@ func NewKeeper(sk [32]byte) *secrets.Keeper { ) } +// Base64Key takes a secret key as a base64 string and converts it +// to a [32]byte, cropping it if necessary. +func Base64Key(base64str string) ([32]byte, error) { + var sk32 [32]byte + key, err := base64.StdEncoding.DecodeString(base64str) + if err != nil { + return sk32, err + } + copy(sk32[:], key) + return sk32, nil +} + // ByteKey takes a secret key as a string and converts it // to a [32]byte, cropping it if necessary. func ByteKey(sk string) [32]byte { diff --git a/secrets/localsecrets/localsecrets_test.go b/secrets/localsecrets/localsecrets_test.go index 714502a8e2..f7373506c9 100644 --- a/secrets/localsecrets/localsecrets_test.go +++ b/secrets/localsecrets/localsecrets_test.go @@ -53,3 +53,24 @@ func (v verifyAs) ErrorCheck(k *secrets.Keeper, err error) error { } return nil } + +func TestOpenKeeper(t *testing.T) { + tests := []struct { + URL string + WantErr bool + }{ + {"stringkey://my-secret", false}, + {"stringkey://my-secret?param=value", true}, + {"base64key://bXktc2VjcmV0LWtleQ==", false}, + {"base64key://bXktc2VjcmV0LWtleQ==?param=value", true}, + {"base64key://not-valid-base64", true}, + } + + ctx := context.Background() + for _, test := range tests { + _, err := secrets.OpenKeeper(ctx, test.URL) + if (err != nil) != test.WantErr { + t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) + } + } +} diff --git a/secrets/vault/example_test.go b/secrets/vault/example_test.go index e97896d353..0ff80f1a45 100644 --- a/secrets/vault/example_test.go +++ b/secrets/vault/example_test.go @@ -19,6 +19,7 @@ import ( "log" "github.com/hashicorp/vault/api" + "gocloud.dev/secrets" "gocloud.dev/secrets/vault" ) @@ -28,7 +29,7 @@ func Example_encrypt() { ctx := context.Background() client, err := vault.Dial(ctx, &vault.Config{ Token: "", - APIConfig: &api.Config{ + APIConfig: api.Config{ Address: "http://127.0.0.1:8200", }, }) @@ -45,3 +46,14 @@ func Example_encrypt() { decrypted, err := keeper.Decrypt(ctx, ciphertext) _ = decrypted } + +func Example_openKeeper() { + ctx := context.Background() + + // OpenKeeper creates a *secrets.Keeper from a URL. + // The host+path are used as the keyID, and the "address" and "token" + // query parameters specify which Vault server to dial, and the auth token + // to use. + k, err := secrets.OpenKeeper(ctx, "vault://MYKEY?address=http://MYVAULT.SERVER.COM:8080&token=MYTOKEN") + _, _ = k, err +} diff --git a/secrets/vault/vault.go b/secrets/vault/vault.go index 92d77b48c2..b8eb5ed302 100644 --- a/secrets/vault/vault.go +++ b/secrets/vault/vault.go @@ -16,6 +16,16 @@ // Engine of Vault by Hashicorp. // Use NewKeeper to construct a *secrets.Keeper. // +// URLs +// +// For secrets.OpenKeeper URLs, vault registers for the scheme "vault"; URLs +// start with "vault://". secrets.OpenKeeper will dial a Vault server once per +// unique combination of the following supported URL parameters: +// - address: Sets Config.APIConfig.Address; should be a full URL with the +// address of the Vault server. +// - token: Sets Config.Token; the access token the Vault client will use. +// Example URL: "vault://mykey?address=http://vault.server.com:8080&token=aaaaa". +// // As // // vault does not support any types for As. @@ -25,7 +35,12 @@ import ( "context" "encoding/base64" "errors" + "fmt" + "net/url" "path" + "sort" + "strings" + "sync" "github.com/hashicorp/vault/api" "gocloud.dev/gcerrors" @@ -37,8 +52,9 @@ type Config struct { // Token is the access token the Vault client uses to talk to the server. // See https://www.vaultproject.io/docs/concepts/tokens.html for more // information. - Token string - APIConfig *api.Config + Token string + // APIConfig is used to configure the creation of the client. + APIConfig api.Config } // Dial gets a Vault client. @@ -46,7 +62,7 @@ func Dial(ctx context.Context, cfg *Config) (*api.Client, error) { if cfg == nil { return nil, errors.New("no auth Config provided") } - c, err := api.NewClient(cfg.APIConfig) + c, err := api.NewClient(&cfg.APIConfig) if err != nil { return nil, err } @@ -56,6 +72,85 @@ func Dial(ctx context.Context, cfg *Config) (*api.Client, error) { return c, nil } +func init() { + secrets.DefaultURLMux().RegisterKeeper(Scheme, new(lazyDialer)) +} + +// lazyDialer lazily dials unique Vault servers. +type lazyDialer struct { + mu sync.Mutex + clients map[string]*api.Client +} + +func (o *lazyDialer) cachedClient(ctx context.Context, u *url.URL) (*api.Client, *url.URL, error) { + o.mu.Lock() + defer o.mu.Unlock() + if o.clients == nil { + o.clients = map[string]*api.Client{} + } + var cfg Config + var cacheKeyParts []string + q := u.Query() + for param, values := range u.Query() { + value := values[0] + switch param { + case "token": + cfg.Token = value + case "address": + cfg.APIConfig.Address = value + default: + continue + } + cacheKeyParts = append(cacheKeyParts, fmt.Sprintf("%s=%s", param, value)) + q.Del(param) + } + sort.Strings(cacheKeyParts) + cacheKey := strings.Join(cacheKeyParts, ",") + client := o.clients[cacheKey] + if client == nil { + var err error + client, err = Dial(ctx, &cfg) + if err != nil { + return nil, nil, err + } + o.clients[cacheKey] = client + } + // Returned an updated URL with the query parameters that we used cleared. + u2 := u + u2.RawQuery = q.Encode() + return client, u2, nil +} + +func (o *lazyDialer) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + client, u2, err := o.cachedClient(ctx, u) + if err != nil { + return nil, err + } + opener := &URLOpener{Client: client} + return opener.OpenKeeperURL(ctx, u2) +} + +// Scheme is the URL scheme vault registers its URLOpener under on secrets.DefaultMux. +const Scheme = "vault" + +// URLOpener opens Vault URLs like "vault://mykey". +// The URL Host + Path are used as the keyID. +type URLOpener struct { + // Client must be non-nil. + Client *api.Client + + // Options specifies the default options to pass to NewKeeper. + Options KeeperOptions +} + +// OpenKeeperURL opens the Keeper URL. +func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) { + for param := range u.Query() { + return nil, fmt.Errorf("open keeper %q: invalid query parameter %q", u, param) + } + return NewKeeper(o.Client, path.Join(u.Host, u.Path), &o.Options), nil +} + // NewKeeper returns a *secrets.Keeper that uses the Transit Secrets Engine of // Vault by Hashicorp. // See the package documentation for an example. diff --git a/secrets/vault/vault_test.go b/secrets/vault/vault_test.go index d17a63e2ef..9791c6deea 100644 --- a/secrets/vault/vault_test.go +++ b/secrets/vault/vault_test.go @@ -17,6 +17,7 @@ package vault import ( "context" "errors" + "net/url" "testing" "github.com/hashicorp/vault/api" @@ -115,7 +116,7 @@ func TestNoConnectionError(t *testing.T) { // doing encryption which should fail by no connection. client, err := Dial(ctx, &Config{ Token: "", - APIConfig: &api.Config{ + APIConfig: api.Config{ Address: apiAddress, }, }) @@ -128,3 +129,89 @@ func TestNoConnectionError(t *testing.T) { t.Error("got nil, want connection refused") } } + +func TestURLCaching(t *testing.T) { + + tests := []struct { + URL string + Want int + }{ + { + URL: "vault://mykey?address=foo&token=bar", + Want: 1, + }, + // Cached. + { + URL: "vault://mykey?address=foo&token=bar", + Want: 1, + }, + // Still cached despite parameter order change. + { + URL: "vault://mykey?token=bar&address=foo", + Want: 1, + }, + // Still cached despite key change. + { + URL: "vault://anotherkey?token=bar&address=foo", + Want: 1, + }, + // Still cached despite extra parameter. + { + URL: "vault://anotherkey?token=bar&address=foo&someparam=somevalue", + Want: 1, + }, + // New token. + { + URL: "vault://mykey?token=newtoken&address=foo", + Want: 2, + }, + // Old is still cached. + { + URL: "vault://mykey?address=foo&token=bar", + Want: 2, + }, + // And new is cached. + { + URL: "vault://mykey?token=newtoken&address=foo", + Want: 2, + }, + // New address. + { + URL: "vault://mykey?token=bar&address=newaddress", + Want: 3, + }, + } + + ctx := context.Background() + o := &lazyDialer{} + for i, test := range tests { + u, err := url.Parse(test.URL) + if err != nil { + t.Fatal(err) + } + o.cachedClient(ctx, u) + if got := len(o.clients); got != test.Want { + t.Errorf("%d/%s: got %d want %d", i, test.URL, got, test.Want) + } + } +} + +func TestOpenKeeper(t *testing.T) { + tests := []struct { + URL string + WantErr bool + }{ + {"vault://mykey?token=bar&address=address", false}, + {"vault://mykey?token=bar&token=token", false}, + {"vault://mykey?token=bar&address=address&token=token", false}, + {"vault://mykey?token=bar¶m=value", true}, + } + + ctx := context.Background() + for _, test := range tests { + _, err := secrets.OpenKeeper(ctx, test.URL) + if (err != nil) != test.WantErr { + t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr) + } + } +}