Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

secrets: add support for URLOpeners to all drivers #1427

Merged
merged 7 commits into from
Mar 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/testing/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,4 @@ func FakeGCPDefaultCredentials(t *testing.T) func() {
os.Remove(f.Name())
os.Setenv(envVar, oldEnvVal)
}
}
}
14 changes: 13 additions & 1 deletion secrets/awskms/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"log"

"github.com/aws/aws-sdk-go/aws/session"
"gocloud.dev/secrets"
"gocloud.dev/secrets/awskms"
)

Expand All @@ -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",
Expand All @@ -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
}
99 changes: 90 additions & 9 deletions secrets/awskms/kms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions secrets/awskms/kms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
12 changes: 12 additions & 0 deletions secrets/gcpkms/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"log"

"gocloud.dev/secrets"
"gocloud.dev/secrets/gcpkms"
)

Expand Down Expand Up @@ -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
}
77 changes: 72 additions & 5 deletions secrets/gcpkms/kms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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{
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions secrets/gcpkms/kms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
15 changes: 15 additions & 0 deletions secrets/localsecrets/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"log"

"gocloud.dev/secrets"
"gocloud.dev/secrets/localsecrets"
)

Expand All @@ -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
}
Loading