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

[WIP] Fix assuming role via ".aws/config" #1590

Closed
wants to merge 2 commits into from
Closed
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
118 changes: 118 additions & 0 deletions aws/auth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aws
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
Expand All @@ -17,6 +18,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/go-ini/ini"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -169,6 +171,27 @@ func GetCredentials(c *Config) (*awsCredentials.Credentials, error) {
}
}

// Try to read AssumeRole data from ~/.aws/config
assumeRoleConfig := readAssumeRoleConfig(c)
if assumeRoleConfig != nil {
creds, err := assumeRoleConfig.assumeRole(c)
if err != nil {
return nil, err
}

value, err := creds.Get()
if err != nil {
return nil, err
}

// One might still want to assume a different role in the provider
// config, so we just update the providers instead of directly
// returning the credentials.
providers = []awsCredentials.Provider{
&awsCredentials.StaticProvider{Value: value},
}
}

// This is the "normal" flow (i.e. not assuming a role)
if c.AssumeRoleARN == "" {
return awsCredentials.NewChainCredentials(providers), nil
Expand Down Expand Up @@ -236,6 +259,101 @@ func GetCredentials(c *Config) (*awsCredentials.Credentials, error) {
return assumeRoleCreds, nil
}

type assumeRoleConfig struct {
RoleARN string
SourceProfile string
ExternalID string
MFASerial string
RoleSessionName string
}

func readAssumeRoleConfig(c *Config) *assumeRoleConfig {
profile := os.Getenv("AWS_PROFILE")
if profile == "" {
profile = c.Profile
}
if profile == "" {
profile = "default"
}

configContent, err := ioutil.ReadFile(c.ConfigFilename)
if err != nil {
log.Printf("[WARN] Unable read config file (%s): %v", c.ConfigFilename, err)
return nil
}

iniData, err := ini.Load(configContent)
if err != nil {
log.Printf("[WARN] Unable parse config file (%s): %v", c.ConfigFilename, err)
return nil
}

section, err := iniData.GetSection(fmt.Sprintf("profile %s", profile))
if err != nil {
log.Printf("[DEBUG] Profile (%s) doesn't exist in config file (%s): %v", profile, c.ConfigFilename, err)
return nil
}

config := assumeRoleConfig{
RoleARN: section.Key("role_arn").String(),
SourceProfile: section.Key("source_profile").String(),
ExternalID: section.Key("external_id").String(),
MFASerial: section.Key("mfa_serial").String(),
RoleSessionName: section.Key("role_session_name").String(),
}

if config.RoleARN == "" || config.SourceProfile == "" {
log.Printf("[INFO] Config file (%s) doesn't have role_arn and source_profile", c.ConfigFilename)
return nil
}

return &config
}

func (a *assumeRoleConfig) assumeRole(c *Config) (*awsCredentials.Credentials, error) {
sharedCredentialsProvider := &awsCredentials.SharedCredentialsProvider{
Filename: c.CredsFilename,
Profile: a.SourceProfile,
}

creds := awsCredentials.NewCredentials(sharedCredentialsProvider)
_, err := creds.Get()
if err != nil {
return nil, fmt.Errorf("Error loading shared credentials for the source profile: %s", err)
}

awsConfig := &aws.Config{
Credentials: creds,
Region: aws.String(c.Region),
MaxRetries: aws.Int(c.MaxRetries),
HTTPClient: cleanhttp.DefaultClient(),
S3ForcePathStyle: aws.Bool(c.S3ForcePathStyle),
}

stsclient := sts.New(session.New(awsConfig))
assumeRoleProvider := &stscreds.AssumeRoleProvider{
Client: stsclient,
RoleARN: a.RoleARN,
RoleSessionName: a.RoleSessionName,
}

if a.ExternalID != "" {
assumeRoleProvider.ExternalID = &a.ExternalID
}

if a.MFASerial != "" {
assumeRoleProvider.SerialNumber = &a.MFASerial
}

creds = awsCredentials.NewCredentials(assumeRoleProvider)
_, err = creds.Get()
if err != nil {
return nil, fmt.Errorf("Error assuming the role: %s", err)
}

return creds, nil
}

func setOptionalEndpoint(cfg *aws.Config) string {
endpoint := os.Getenv("AWS_METADATA_URL")
if endpoint != "" {
Expand Down
88 changes: 88 additions & 0 deletions aws/auth_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,94 @@ func TestAWSGetCredentials_shouldBeENV(t *testing.T) {
}
}

func TestAWSReadAssumeRoleConfig(t *testing.T) {
cases := []struct {
Contents string
Result *assumeRoleConfig
}{
{
Contents: `[profile myprofile]
role_arn = arn:aws:iam::1234567890:role/admin
source_profile = default
external_id = 123
mfa_serial = 456
role_session_name = mysession`,
Result: &assumeRoleConfig{
RoleARN: "arn:aws:iam::1234567890:role/admin",
SourceProfile: "default",
ExternalID: "123",
MFASerial: "456",
RoleSessionName: "mysession",
},
},
{
Contents: `[profile myprofile]
role_arn = arn:aws:iam::1234567890:role/admin
source_profile = default`,
Result: &assumeRoleConfig{
RoleARN: "arn:aws:iam::1234567890:role/admin",
SourceProfile: "default",
},
},
{
Contents: `[profile myprofile]
source_profile = default`,
Result: nil,
},
{
Contents: `[profile myprofile]
role_arn = arn:aws:iam::1234567890:role/admin`,
Result: nil,
},
{
Contents: ``,
Result: nil,
},
{
Contents: `[myprofile]
role_arn = arn:aws:iam::1234567890:role/admin
source_profile = default`,
Result: nil,
},
{
Contents: `[profile other]
role_arn = arn:aws:iam::1234567890:role/admin
source_profile = default`,
Result: nil,
},
}

for _, c := range cases {
file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_config")
if err != nil {
t.Fatalf("Error writing temporary config file: %s", err)
}
_, err = file.WriteString(c.Contents)
if err != nil {
t.Fatalf("Error writing temporary config to file: %s", err)
}
err = file.Close()
if err != nil {
t.Fatalf("Error closing temporary config file: %s", err)
}
defer os.Remove(file.Name())

assumeRoleConfig := readAssumeRoleConfig(&Config{
Profile: "myprofile",
ConfigFilename: file.Name(),
})

if c.Result == nil && assumeRoleConfig == nil {
continue
}

if *c.Result != *assumeRoleConfig {
t.Errorf("Config mismatch, expected: %#v, got %#v", c.Result, assumeRoleConfig)
}

}
}

func testGetAccountInfo(t *testing.T, iamSess, stsSess *session.Session, credProviderName string) {

iamConn := iam.New(iamSess)
Expand Down
15 changes: 8 additions & 7 deletions aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,14 @@ import (
)

type Config struct {
AccessKey string
SecretKey string
CredsFilename string
Profile string
Token string
Region string
MaxRetries int
AccessKey string
SecretKey string
CredsFilename string
ConfigFilename string
Profile string
Token string
Region string
MaxRetries int

AssumeRoleARN string
AssumeRoleExternalID string
Expand Down
16 changes: 16 additions & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ func Provider() terraform.ResourceProvider {
Description: descriptions["shared_credentials_file"],
},

"shared_config_file": {
Type: schema.TypeString,
Optional: true,
Default: "~/.aws/config",
Description: descriptions["shared_config_file"],
},

"token": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -509,6 +516,9 @@ func init() {
"shared_credentials_file": "The path to the shared credentials file. If not set\n" +
"this defaults to ~/.aws/credentials.",

"shared_config_file": "The path to the shared config file. If not set\n" +
"this defaults to ~/.aws/config.",

"token": "session token. A session token is only required if you are\n" +
"using temporary security credentials.",

Expand Down Expand Up @@ -609,6 +619,12 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
}
config.CredsFilename = credsPath

configPath, err := homedir.Expand(d.Get("shared_config_file").(string))
if err != nil {
return nil, err
}
config.ConfigFilename = configPath

assumeRoleList := d.Get("assume_role").(*schema.Set).List()
if len(assumeRoleList) == 1 {
assumeRole := assumeRoleList[0].(map[string]interface{})
Expand Down
3 changes: 3 additions & 0 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ The following arguments are supported in the `provider` block:
* `shared_credentials_file` = (Optional) This is the path to the shared credentials file.
If this is not set and a profile is specified, `~/.aws/credentials` will be used.

* `shared_config_file` = (Optional) This is the path to the shared config file.
If this is not set and a profile is specified, `~/.aws/config` will be used.

* `token` - (Optional) Use this to set an MFA token. It can also be sourced
from the `AWS_SESSION_TOKEN` environment variable.

Expand Down