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

Allow assuming a role via AWS_PROFILE or configuration files #580

Merged
merged 4 commits into from
May 31, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

### Improvements

- Addition of enums for SSM Parameter Types
- Add an enumeration for SSM Parameter Types
- Fixed a bug where `AWS_PROFILE` and the ~/.aws/config files were not honoured.

## 0.18.6 (Released May 24th, 2019)

Expand Down
2 changes: 2 additions & 0 deletions examples/switchrole/create-user-and-role/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
3 changes: 3 additions & 0 deletions examples/switchrole/create-user-and-role/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: create-user-and-role
runtime: nodejs
description: Demonstrate use of AWS profiles for role switching
37 changes: 37 additions & 0 deletions examples/switchrole/create-user-and-role/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const unprivilegedUsername = config.require("unprivilegedUsername");

const unprivilegedUser = new aws.iam.User("unprivileged-user", {
name: unprivilegedUsername,
});

const unprivilegedUserCreds = new aws.iam.AccessKey("unprivileged-user-key", {
user: unprivilegedUser.name,
});

const allowS3ManagementRole = new aws.iam.Role("allow-s3-management", {
description: "Allow management of S3 buckets",
assumeRolePolicy: unprivilegedUser.arn.apply(arn => {
return aws.iam.assumeRolePolicyForPrincipal({AWS: arn})
}),
});

new aws.iam.RolePolicy("allow-s3-management-policy", {
role: allowS3ManagementRole,
policy: {
Version: "2012-10-17",
Statement: [{
Sid: "AllowS3Management",
Effect: "Allow",
Resource: "*",
Action: "s3:*",
}]
}
}, {parent: allowS3ManagementRole});

export const roleArn = allowS3ManagementRole.arn;
export const accessKeyId = unprivilegedUserCreds.id;
export const secretAccessKey = unprivilegedUserCreds.secret;
10 changes: 10 additions & 0 deletions examples/switchrole/create-user-and-role/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "typescript",
"devDependencies": {
"@types/node": "latest"
},
"dependencies": {
"@pulumi/aws": "^0.18.6",
"@pulumi/pulumi": "latest"
}
}
22 changes: 22 additions & 0 deletions examples/switchrole/create-user-and-role/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}
2 changes: 2 additions & 0 deletions examples/switchrole/use-role/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
3 changes: 3 additions & 0 deletions examples/switchrole/use-role/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: use-role
runtime: nodejs
description: Use the created role
5 changes: 5 additions & 0 deletions examples/switchrole/use-role/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("created-with-role");

export const bucketArn = bucket.arn;
10 changes: 10 additions & 0 deletions examples/switchrole/use-role/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "typescript",
"devDependencies": {
"@types/node": "latest"
},
"dependencies": {
"@pulumi/aws": "^0.18.6",
"@pulumi/pulumi": "latest"
}
}
22 changes: 22 additions & 0 deletions examples/switchrole/use-role/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}
19 changes: 7 additions & 12 deletions resources.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016-2018, Pulumi Corporation.(
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -191,17 +191,7 @@ func preConfigureCallback(vars resource.PropertyMap, c *terraform.ResourceConfig
}
config.CredsFilename = credsPath

// TODO[pulumi/pulumi-terraform#48] We should also be setting `config.AssumeRole*` here, but we are currently
// blocked on not being able to read out list-valued provider config.

creds, err := awsbase.GetCredentials(config)
if err != nil {
return errors.New("unable to discover AWS AccessKeyID and/or SecretAccessKey " +
"- see https://pulumi.io/install/aws.html for details on configuration")
}

_, err = creds.Get()
if err != nil {
if _, err := awsbase.GetCredentials(config); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow why this is the right fix.

All of the code here is intended to catch and report failures to authenticate prior to the Terraform Provider Configure call reporting back failures. But the goal is to do the same checks the Terraform Provider will do.

Per https://github.com/hashicorp/aws-sdk-go-base/blob/8fac28668e59a2e5afa6b2d034614c648ddc418e/session.go#L31, it appears the call to creds.Get() will be made there.

Is the problem instead that Profile: stringValue(vars, "profile"), isn't correctly reading the profile here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason using AWS_PROFILE was failing is because we weren't reading the profile correctly before - whether through a profile configured in ~/.aws/config or ~/.aws/credentials.

I don't think removing the call to Get on the returned Credentials struct is strictly necessary to fix the bug, but I also don't think it buys us anything to do it. GetCredentials itself calls Get here (after constructing a valid credentials chain):

https://github.com/hashicorp/aws-sdk-go-base/blob/master/awsauth.go#L239

It also tests that a role can be assumed, if appropriate:

https://github.com/hashicorp/aws-sdk-go-base/blob/master/awsauth.go#L283

If we hit either of these cases, the call to GetCredentials would have failed before anyway, so Get would not be called. If it worked the first time (in GetCredentials), calling it a second time should not yield different behaviour without configuration changes.

It's worth noting that the aws-sdk-go-base library is newer than the Pulumi AWS provider, and that the checks done by Terraform have changed as a result of using this library.

Finally, the error message we produce in response to any error from configuration complains about AccessKeyID and SecretAccessKey. In future, we should change the error to reflect the actual source of the problem. We could duplicate the entire logic of GetCredentials in the pre-configure callback to give better error messages here, but it like we'd be better off doing string detection on the error values to avoid doing so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think removing the call to Get on the returned Credentials struct is strictly necessary to fix the bug, but I also don't think it buys us anything to do it.

Got it - makes sense.

Finally, the error message we produce in response to any error from configuration complains about AccessKeyID and SecretAccessKey. In future, we should change the error to reflect the actual source of the problem. We could duplicate the entire logic of GetCredentials in the pre-configure callback to give better error messages here, but it like we'd be better off doing string detection on the error values to avoid doing so.

Totally agreed.

return errors.New("unable to discover AWS AccessKeyID and/or SecretAccessKey " +
"- see https://pulumi.io/install/aws.html for details on configuration")
}
Expand Down Expand Up @@ -230,6 +220,11 @@ func Provider() tfbridge.ProviderInfo {
EnvVars: []string{"AWS_REGION", "AWS_DEFAULT_REGION"},
},
},
"profile": {
Default: &tfbridge.DefaultInfo{
EnvVars: []string{"AWS_PROFILE"},
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are quite a few more config vars that can be populated from AWS_* env vars - should we add the rest?

https://www.terraform.io/docs/providers/aws/index.html#environment-variables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should - I'll follow up on this with a new PR soon.

},
PreConfigureCallback: preConfigureCallback,
Resources: map[string]*tfbridge.ResourceInfo{
Expand Down
9 changes: 8 additions & 1 deletion sdk/go/aws/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ func GetMaxRetries(ctx *pulumi.Context) int {

// The profile for API operations. If not set, the default profile created with `aws configure` will be used.
func GetProfile(ctx *pulumi.Context) string {
return config.Get(ctx, "aws:profile")
v, err := config.Try(ctx, "aws:profile")
if err == nil {
return v
}
if dv, ok := getEnvOrDefault("", nil, "AWS_PROFILE").(string); ok {
return dv
}
return v
}

// The region where AWS operations will take place. Examples are us-east-1, us-west-2, etc.
Expand Down
2 changes: 1 addition & 1 deletion sdk/nodejs/config/vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export let maxRetries: number | undefined = __config.getObject<number>("maxRetri
/**
* The profile for API operations. If not set, the default profile created with `aws configure` will be used.
*/
export let profile: string | undefined = __config.get("profile");
export let profile: string | undefined = __config.get("profile") || utilities.getEnv("AWS_PROFILE");
/**
* The region where AWS operations will take place. Examples are us-east-1, us-west-2, etc.
*/
Expand Down
2 changes: 1 addition & 1 deletion sdk/nodejs/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Provider extends pulumi.ProviderResource {
inputs["forbiddenAccountIds"] = pulumi.output(args ? args.forbiddenAccountIds : undefined).apply(JSON.stringify);
inputs["insecure"] = pulumi.output(args ? args.insecure : undefined).apply(JSON.stringify);
inputs["maxRetries"] = pulumi.output(args ? args.maxRetries : undefined).apply(JSON.stringify);
inputs["profile"] = args ? args.profile : undefined;
inputs["profile"] = (args ? args.profile : undefined) || utilities.getEnv("AWS_PROFILE");
inputs["region"] = (args ? args.region : undefined) || utilities.getEnv("AWS_REGION", "AWS_DEFAULT_REGION");
inputs["s3ForcePathStyle"] = pulumi.output(args ? args.s3ForcePathStyle : undefined).apply(JSON.stringify);
inputs["secretKey"] = args ? args.secretKey : undefined;
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/pulumi_aws/config/vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
The maximum number of times an AWS API request is being executed. If the API request still fails, an error is thrown.
"""

profile = __config__.get('profile')
profile = __config__.get('profile') or utilities.get_env('AWS_PROFILE')
"""
The profile for API operations. If not set, the default profile created with `aws configure` will be used.
"""
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/pulumi_aws/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def __init__(__self__, resource_name, opts=None, access_key=None, allowed_accoun

__props__['max_retries'] = pulumi.Output.from_input(max_retries).apply(json.dumps) if max_retries is not None else None

if profile is None:
profile = utilities.get_env('AWS_PROFILE')
__props__['profile'] = profile

if region is None:
Expand Down