diff --git a/aws/credentials/plugincreds/doc_1_7.go b/aws/credentials/plugincreds/doc_1_7.go new file mode 100644 index 00000000000..5b269feeba6 --- /dev/null +++ b/aws/credentials/plugincreds/doc_1_7.go @@ -0,0 +1,5 @@ +// +build !go1.8 + +// Package plugincreds provides usage of Go plugins for providing credentials +// to the SDK. Only available with Go 1.8 and above. +package plugincreds diff --git a/aws/credentials/plugincreds/provider.go b/aws/credentials/plugincreds/provider.go new file mode 100644 index 00000000000..a66f1ddf14f --- /dev/null +++ b/aws/credentials/plugincreds/provider.go @@ -0,0 +1,210 @@ +// +build go1.8 + +// Package plugincreds implements a credentials provider sourced from a Go +// plugin. This package allows you to use a Go plugin to retrieve AWS credentials +// for the SDK to use for service API calls. +// +// As of Go 1.8 plugins are only supported on the Linux platform. +// +// Plugin Symbol Name +// +// See ProviderSymbolName for the symbol named that will be used to lookup the +// credentials plugin provider. If you want to use a custom symbol name you +// should use GetPluginProviderFnsByName to lookup the symbol by a custom name. +// +// This symbol is a function that returns two additional functions. One to +// retrieve the credentials, and another to determine if the credentials have +// expired. +// +// Plugin Symbol Signature +// +// The plugin credential provider requires the symbol to match the +// following signature. +// +// func() (RetrieveFn func() (key, secret, token string, err error), IsExpiredFn func() bool) +// +// Plugin Implementation Exmaple +// +// The following is an example implementation of a SDK credential provider using +// the plugin provider in this package. See the SDK's example/aws/credential/plugincreds/plugin +// folder for a runnable example of this. +// +// package main +// +// func main() {} +// +// var myCredProvider provider +// +// // Build: go build -o plugin.so -buildmode=plugin plugin.go +// func init() { +// // Initialize a mock credential provider with stubs +// myCredProvider = provider{"a","b","c"} +// } +// +// // GetAWSSDKCredentialProvider is the symbol SDK will lookup and use to +// // get the credential provider's retrieve and isExpired functions. +// func GetAWSSDKCredentialProvider() (func() (key, secret, token string, err error), func() bool) { +// return myCredProvider.Retrieve, myCredProvider.IsExpired +// } +// +// // mock implementation of a type that returns retrieves credentials and +// // returns if they have expired. +// type provider struct { +// key, secret, token string +// } +// +// func (p provider) Retrieve() (key, secret, token string, err error) { +// return p.key, p.secret, p.token, nil +// } +// +// func (p *provider) IsExpired() bool { +// return false; +// } +// +// Configuring SDK for Plugin Credentials +// +// To configure the SDK to use a plugin's credential provider you'll need to first +// open the plugin file using the plugin standard library package. Once you have +// a handle to the plugin you can use the NewCredentials function of this package +// to create a new credentials.Credentials value that can be set as the +// credentials loader of a Session or Config. See the SDK's example/aws/credential/plugincreds +// folder for a runnable example of this. +// +// // Open plugin, and load it into the process. +// p, err := plugin.Open("somefile.so") +// if err != nil { +// return nil, err +// } +// +// // Create a new Credentials value which will source the provider's Retrieve +// // and IsExpired functions from the plugin. +// creds, err := plugincreds.NewCredentials(p) +// if err != nil { +// return nil, err +// } +// +// // Example to configure a Session with the newly created credentials that +// // will be sourced using the plugin's functionality. +// sess := session.Must(session.NewSession(&aws.Config{ +// Credentials: creds, +// })) +package plugincreds + +import ( + "fmt" + "plugin" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" +) + +// ProviderSymbolName the symbol name the SDK will use to lookup the plugin +// provider value from. +const ProviderSymbolName = `GetAWSSDKCredentialProvider` + +// ProviderName is the name this credentials provider will label any returned +// credentials Value with. +const ProviderName = `PluginCredentialsProvider` + +const ( + // ErrCodeLookupSymbolError failed to lookup symbol + ErrCodeLookupSymbolError = "LookupSymbolError" + + // ErrCodeInvalidSymbolError symbol invalid + ErrCodeInvalidSymbolError = "InvalidSymbolError" + + // ErrCodePluginRetrieveNil Retrieve function was nil + ErrCodePluginRetrieveNil = "PluginRetrieveNilError" + + // ErrCodePluginIsExpiredNil IsExpired Function was nil + ErrCodePluginIsExpiredNil = "PluginIsExpiredNilError" + + // ErrCodePluginProviderRetrieve plugin provider's retrieve returned error + ErrCodePluginProviderRetrieve = "PluginProviderRetrieveError" +) + +// Provider is the credentials provider that will use the plugin provided +// Retrieve and IsExpired functions to retrieve credentials. +type Provider struct { + RetrieveFn func() (key, secret, token string, err error) + IsExpiredFn func() bool +} + +// NewCredentials returns a new Credentials loader using the plugin provider. +// If the symbol isn't found or is invalid in the plugin an error will be +// returned. +func NewCredentials(p *plugin.Plugin) (*credentials.Credentials, error) { + retrieve, isExpired, err := GetPluginProviderFns(p) + if err != nil { + return nil, err + } + + return credentials.NewCredentials(Provider{ + RetrieveFn: retrieve, + IsExpiredFn: isExpired, + }), nil +} + +// Retrieve will return the credentials Value if they were successfully retrieved +// from the underlying plugin provider. An error will be returned otherwise. +func (p Provider) Retrieve() (credentials.Value, error) { + creds := credentials.Value{ + ProviderName: ProviderName, + } + + k, s, t, err := p.RetrieveFn() + if err != nil { + return creds, awserr.New(ErrCodePluginProviderRetrieve, + "failed to retrieve credentials with plugin provider", err) + } + + creds.AccessKeyID = k + creds.SecretAccessKey = s + creds.SessionToken = t + + return creds, nil +} + +// IsExpired will return the expired state of the underlying plugin provider. +func (p Provider) IsExpired() bool { + return p.IsExpiredFn() +} + +// GetPluginProviderFns returns the plugin's Retrieve and IsExpired functions +// returned by the plugin's credential provider getter. +// +// Uses ProviderSymbolName as the symbol name when lookup up the symbol. If you +// want to use a different symbol name, use GetPluginProviderFnsByName. +func GetPluginProviderFns(p *plugin.Plugin) (func() (key, secret, token string, err error), func() bool, error) { + return GetPluginProviderFnsByName(p, ProviderSymbolName) +} + +// GetPluginProviderFnsByName returns the plugin's Retrieve and IsExpired functions +// returned by the plugin's credential provider getter. +// +// Same as GetPluginProviderFns, but takes a custom symbolName to lookup with. +func GetPluginProviderFnsByName(p *plugin.Plugin, symbolName string) (func() (key, secret, token string, err error), func() bool, error) { + sym, err := p.Lookup(symbolName) + if err != nil { + return nil, nil, awserr.New(ErrCodeLookupSymbolError, + fmt.Sprintf("failed to lookup %s plugin provider symbol", symbolName), err) + } + + fn, ok := sym.(func() (func() (key, secret, token string, err error), func() bool)) + if !ok { + return nil, nil, awserr.New(ErrCodeInvalidSymbolError, + fmt.Sprintf("symbol %T, does not match the 'func() (func() (key, secret, token string, err error), func() bool)' type", sym), nil) + } + + retrieveFn, isExpiredFn := fn() + if retrieveFn == nil { + return nil, nil, awserr.New(ErrCodePluginRetrieveNil, + "the plugin provider retrieve function cannot be nil", nil) + } + if isExpiredFn == nil { + return nil, nil, awserr.New(ErrCodePluginIsExpiredNil, + "the plugin provider isExpired function cannot be nil", nil) + } + + return retrieveFn, isExpiredFn, nil +} diff --git a/aws/credentials/plugincreds/provider_test.go b/aws/credentials/plugincreds/provider_test.go new file mode 100644 index 00000000000..637b6548429 --- /dev/null +++ b/aws/credentials/plugincreds/provider_test.go @@ -0,0 +1,71 @@ +// +build go1.8 + +package plugincreds + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" +) + +func TestProvider_Passthrough(t *testing.T) { + p := Provider{ + RetrieveFn: func() (string, string, string, error) { + return "key", "secret", "token", nil + }, + IsExpiredFn: func() bool { + return false + }, + } + + actual, err := p.Retrieve() + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + expect := credentials.Value{ + AccessKeyID: "key", + SecretAccessKey: "secret", + SessionToken: "token", + ProviderName: ProviderName, + } + if expect != actual { + t.Errorf("expect %+v credentials, got %+v", expect, actual) + } +} + +func TestProvider_Error(t *testing.T) { + expectErr := fmt.Errorf("expect error") + + p := Provider{ + RetrieveFn: func() (string, string, string, error) { + return "", "", "", expectErr + }, + IsExpiredFn: func() bool { + return false + }, + } + + actual, err := p.Retrieve() + if err == nil { + t.Fatalf("expect error, got none") + } + + aerr := err.(awserr.Error) + if e, a := ErrCodePluginProviderRetrieve, aerr.Code(); e != a { + t.Errorf("expect %s error code, got %s", e, a) + } + + if e, a := expectErr, aerr.OrigErr(); e != a { + t.Errorf("expect %v cause error, got %v", e, a) + } + + expect := credentials.Value{ + ProviderName: ProviderName, + } + if expect != actual { + t.Errorf("expect %+v credentials, got %+v", expect, actual) + } +} diff --git a/example/aws/credentials/plugincreds/README.md b/example/aws/credentials/plugincreds/README.md new file mode 100644 index 00000000000..429c83b9800 --- /dev/null +++ b/example/aws/credentials/plugincreds/README.md @@ -0,0 +1,32 @@ +Retrieve Credentials with Go Plugin +=== + +This example demonstrates how you can take advantage of Go 1.8's new Plugin +functionality to retrieve AWS credentials dynamically from a plugin compiled +separate from your application. + +Usage +--- + +Example Plugin +--- + +You can find the plugin at `plugin/plugin.go` nested within this example. The plugin +demonstrates what symbol the SDK will use when lookup up the credential provider +and the type signature that needs to be implemented. + +Compile the plugin with: + + go build -tags example -o plugin.so -buildmode=plugin plugin.go + +Example Application +--- + +The `main.go` file in this folder demonstrates how you can configure the SDK to +use a plugin to retrieve credentials with. + +Compile and run application: + + go build -tags example -o usePlugin usePlugin.go + + ./usePlugin ./plugin/plugin diff --git a/example/aws/credentials/plugincreds/main.go b/example/aws/credentials/plugincreds/main.go new file mode 100644 index 00000000000..d55e3038e0b --- /dev/null +++ b/example/aws/credentials/plugincreds/main.go @@ -0,0 +1,61 @@ +// +build example,go18 + +package main + +import ( + "fmt" + "os" + "plugin" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/plugincreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" +) + +// Example application which loads a Go Plugin file, and uses the credential +// provider defined within the plugin to get credentials for making a S3 +// request. +// +// Build: +// go build -tags example -o usePlugin main.go +// +// Usage: +// ./usePlugin +func main() { + if len(os.Args) < 2 { + exitErrorPrintf("Usage: usePlugin ") + } + + // Open plugin, and load it into the process. + p, err := plugin.Open(os.Args[1]) + if err != nil { + exitErrorPrintf("failed to open plugin, %s, %v", os.Args[1], err) + } + + // Create a new Credentials value which will source the provider's Retrieve + // and IsExpired functions from the plugin. + creds, err := plugincreds.NewCredentials(p) + if err != nil { + exitErrorPrintf("failed to load plugin provider, %v", err) + } + + // Example to configure a Session with the newly created credentials that + // will be sourced using the plugin's functionality. + sess := session.Must(session.NewSession(&aws.Config{ + Credentials: creds, + })) + + svc := s3.New(sess) + result, err := svc.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String("myBucket"), + Key: aws.String("myKey"), + }) + + fmt.Println(result, err) +} + +func exitErrorPrintf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} diff --git a/example/aws/credentials/plugincreds/plugin/plugin.go b/example/aws/credentials/plugincreds/plugin/plugin.go new file mode 100644 index 00000000000..d61177efcd2 --- /dev/null +++ b/example/aws/credentials/plugincreds/plugin/plugin.go @@ -0,0 +1,36 @@ +// +build example,go18 + +package main + +// Example plugin +// +// Build with: +// go build -tags example -o plugin.so -buildmode=plugin plugin.go +func main() {} + +var myCredProvider provider + +func init() { + // Initialize a mock credential provider with stubs + myCredProvider = provider{"a", "b", "c"} +} + +// GetAWSSDKCredentialProvider is the symbol SDK will lookup and use to +// get the credential provider's retrieve and isExpired functions. +func GetAWSSDKCredentialProvider() (func() (key, secret, token string, err error), func() bool) { + return myCredProvider.Retrieve, myCredProvider.IsExpired +} + +// mock implementation of a type that returns retrieves credentials and +// returns if they have expired. +type provider struct { + key, secret, token string +} + +func (p provider) Retrieve() (key, secret, token string, err error) { + return p.key, p.secret, p.token, nil +} + +func (p *provider) IsExpired() bool { + return false +}