Skip to content

Commit

Permalink
aws/credentials/plugincreds: Add support for Go plugin for credentials (
Browse files Browse the repository at this point in the history
#1320)

Adds support for using plugins to retrieve credentials for API requests.
This change adds a new package plugincreds under aws/credentials.

As of Go 1.8 this functionality is only available for Linux.

See the example/aws/credentials/plugincreds folder in the SDK for
example usage.
  • Loading branch information
jasdel authored Jun 19, 2017
1 parent 34275dd commit 834ee94
Show file tree
Hide file tree
Showing 6 changed files with 415 additions and 0 deletions.
5 changes: 5 additions & 0 deletions aws/credentials/plugincreds/doc_1_7.go
Original file line number Diff line number Diff line change
@@ -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
210 changes: 210 additions & 0 deletions aws/credentials/plugincreds/provider.go
Original file line number Diff line number Diff line change
@@ -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
}
71 changes: 71 additions & 0 deletions aws/credentials/plugincreds/provider_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
32 changes: 32 additions & 0 deletions example/aws/credentials/plugincreds/README.md
Original file line number Diff line number Diff line change
@@ -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
61 changes: 61 additions & 0 deletions example/aws/credentials/plugincreds/main.go
Original file line number Diff line number Diff line change
@@ -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 <compiled plugin>
func main() {
if len(os.Args) < 2 {
exitErrorPrintf("Usage: usePlugin <compiled plugin>")
}

// 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)
}
Loading

0 comments on commit 834ee94

Please sign in to comment.