Repository: Versent/saml2aws
Pull request #336 (2019-08-19 21:52:53)

#335 Proposal to allow `saml2aws exec` against specific AWS profile
Proposal for discussion regarding issue #335 

See README for how to utilize the proposed flag.


Patch:
diff --git a/README.md b/README.md
index 6e71352..bc79ec3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 # saml2aws [![Build Status](https://travis-ci.org/Versent/saml2aws.svg?branch=master)](https://travis-ci.org/Versent/saml2aws) [![Build status - Windows](https://ci.appveyor.com/api/projects/status/ptpi18kci16o4i82/branch/master?svg=true)](https://ci.appveyor.com/project/davidobrien1985/saml2aws/branch/master)
 
-CLI tool which enables you to login and retrieve [AWS](https://aws.amazon.com/) temporary credentials using SAML with [ADFS](https://msdn.microsoft.com/en-us/library/bb897402.aspx) or [PingFederate](https://www.pingidentity.com/en/products/pingfederate.html) Identity Providers.
+CLI tool which enables you to login and retrieve [AWS](https://aws.amazon.com/) temporary credentials using 
+with [ADFS](https://msdn.microsoft.com/en-us/library/bb897402.aspx) or [PingFederate](https://www.pingidentity.com/en/products/pingfederate.html) Identity Providers.
 
 This is based on python code from [
 How to Implement a General Solution for Federated API/CLI Access Using SAML 2.0](https://blogs.aws.amazon.com/security/post/TxU0AVUS9J00FP/How-to-Implement-a-General-Solution-for-Federated-API-CLI-Access-Using-SAML-2-0).
@@ -117,6 +118,7 @@ Flags:
                                SAML2AWS_AWS_URN)
       --duo-mfa-option         The MFA option you want to use to authenticate (env: SAML_DUO_MFA_OPTION)
       --skip-prompt            Skip prompting for parameters during login.
+      --exec-profile           Execute the given command utilizing a specific profile from your ~/.aws/config file
       --session-duration=SESSION-DURATION
                                The duration of your AWS Session. (env:
                                SAML2AWS_SESSION_DURATION)
@@ -167,6 +169,17 @@ bash:
 function s2a { eval $( $(which saml2aws) script --shell=bash --profile=$@); }
 ```
 
+### `saml2aws exec`
+
+If the `exec` sub-command is called, `saml2aws` will execute the command given as an argument:
+By default saml2aws will execute the command with temp credentials generated via `saml2aws login`.
+
+The `--exec-profile` flag allows for a command to execute using an aws profile which may have chained "assume role" actions. (via 'source_profile' in ~/.aws/config) *See section "blah" for scenario where this is useful as well as example below.
+
+```
+options:
+--exec-profile           Execute the given command utilizing a specific profile from your ~/.aws/config file
+```
 
 ### Configuring IDP Accounts
 
@@ -266,7 +279,7 @@ To use this credential, call the AWS CLI with the --profile option (e.g. aws --p
 
 ## Advanced Configuration
 
-Configuring multiple accounts with custom role and profile in `~/.aws/config` with goal being isolation between infra code when deploying to these environments. This setup assumes your using separate roles and probably AWS accounts for `dev` and `test` and is designed to help operations staff avoid accidentally deploying to the wrong AWS account in complex environments.
+Configuring multiple accounts with custom role and profile in `~/.aws/config` with goal being isolation between infra code when deploying to these environments. This setup assumes you're using separate roles and probably AWS accounts for `dev` and `test` and is designed to help operations staff avoid accidentally deploying to the wrong AWS account in complex environments. Note that this method configures SAML authentication to each AWS account directly (in this case different AWS accounts). In the example below, separate authentication values are configured for AWS accounts 'profile=customer-dev/awsAccount=was 121234567890' and 'profile=customer-test/awsAccount=121234567891'
 
 ### Dev Account Setup
 
@@ -320,6 +333,102 @@ role_arn                = arn:aws:iam::121234567891:role/customer-admin-role
 
 To use this you will need to export `AWS_DEFAULT_PROFILE=customer-test` environment variable to target `test`.
 
+## Advanced Configuration (Multiple AWS account access but SAML authenticate against a single 'SSO' AWS account)
+
+Example:
+(Authenticate to my 'SSO' AWS account. With this setup, there is no need to authenticate again. We can now rely on IAM to assume role cross account)
+
+~/.aws/credentials: #(these are generated by `saml2aws login`. Sets up SAML authentication into my AWS 'SSO' account)
+```
+[saml]
+aws_access_key_id        = AAAAAAAAAAAAAAAAB
+aws_secret_access_key    = duqhdZPRjEdZPRjE=dZPRjEhKjfB
+aws_session_token        = #REMOVED#
+aws_security_token       = #REMOVED#
+x_principal_arn          = arn:aws:sts::000000000123:assumed-role/myInitialAccount
+x_security_token_expires = 2019-08-19T15:00:56-06:00
+```
+
+(Use AWS profiles to assume an aws role cross-account)
+(Note that the "source_profile" is set to SAML which is my SSO AWS account since it is already authenticated)
+
+~/.aws/config:
+```
+[profile roleIn2ndAwsAccount]
+source_profile=saml
+role_arn=arn:aws:iam::123456789012:role/OtherRoleInAnyFederatedAccount # Note the different account number here
+role_session_name=myAccountName
+```
+
+Running saml2aws without --exec-profile flag:
+```
+saml2aws exec aws sts get-caller-identity
+{
+    "UserId": "AROAYAROAYAROAYOO:myInitialAccount",
+    "Account": "000000000123",
+    "Arn": "arn:aws:sts::000000000123:assumed-role/myInitialAccount"  # This shows my 'SSO' account (SAML profile)
+}
+
+```
+
+Running saml2aws with --exec-profile flag:
+```
+saml2aws exec --exec-profile roleIn2ndAwsAccount aws sts get-caller-identity
+{
+    "UserId": "YOOYOOYOOYOOYOOA:/myAccountName",
+    "Account": "123456789012",
+    "Arn": "arn:aws:sts::123456789012:assumed-role/myAccountName"  # When using '--exec-profile' I can assume-role into a                                                                        # different AWS account without re-authenticating. 
+                                                                   # Note that it does not re-authenitcate since we are 
+                                                                   # alredy authenticated via the SSO account
+}
+```
+
+As an example
+
+```
+saml2aws login
+
+aws s3 ls --profile saml
+
+An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied
+# This is denied in this example because there are no S3 buckets in the 'SSO' AWS account
+
+saml2aws exec --exec-profile roleIn2ndAwsAccount aws s3 ls  # Runs given CMD with environment configured from --exec-profile role
+
+# If we check env variables we see that our environment is configured with temporary credentials for our 'assumed role'
+env | grep AWS
+AWS_SESSION_TTL=12h
+AWS_FEDERATION_TOKEN_TTL=12h
+AWS_ASSUME_ROLE_TTL=1h
+AWS_ACCESS_KEY_ID=AAAAAAAASORTENED
+AWS_SECRET_ACCESS_KEY=secretShortened+6jJ5SMqsM5CkYi3Gw7
+AWS_SESSION_TOKEN=ShortenedTokenXXX=
+AWS_SECURITY_TOKEN=ShortenedSecurityTokenXXX=
+
+# If we desire to execute multiple commands utilizing our assumed profile, we can obtain a new shell with Env variables configured for access
+
+saml2aws exec --exec-profile roleIn2ndAwsAccount $SHELL  # Get a new shell with AWS env vars configured for 'assumed role' account access
+
+# We are now able to execute AWS cli commands with our assume role permissions
+
+# Note that we do not need a --profile flag because our environment variables were set up for this access when we obtained a new shell with the --exec-profile flag
+
+aws s3 ls  
+2019-07-30 01:32:59 264998d7606497040-sampleBucket
+
+aws iam list-groups
+{
+    "Groups": [
+        {
+            "Path": "/",
+            "GroupName": "MyGroup",
+            "GroupId": "AGAGTENTENTENGOCQFK",
+            "Arn": "arn:aws:iam::123456789012:group/MyGroup",
+            "CreateDate": "2019-05-13T16:12:19Z"
+            ]
+        }
+}
+
 ## Building
 
 To build this software on osx clone to the repo to `$GOPATH/src/github.com/versent/saml2aws` and ensure you have `$GOPATH/bin` in your `$PATH`.
@@ -401,3 +510,4 @@ DUMP_CONTENT=true saml2aws login --verbose
 # License
 
 This code is Copyright (c) 2018 [Versent](http://versent.com.au) and released under the MIT license. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE.md file for more details.
+
diff --git a/cmd/saml2aws/commands/exec.go b/cmd/saml2aws/commands/exec.go
index d996dcb..3ec2720 100644
--- a/cmd/saml2aws/commands/exec.go
+++ b/cmd/saml2aws/commands/exec.go
@@ -60,7 +60,7 @@ func Exec(execFlags *flags.LoginExecFlags, cmdline []string) error {
 		return errors.Wrap(err, "error logging in")
 	}
 
-	return shell.ExecShellCmd(cmdline, shell.BuildEnvVars(awsCreds, account))
+	return shell.ExecShellCmd(cmdline, shell.BuildEnvVars(awsCreds, account, execFlags))
 }
 
 func checkToken(profile string) (bool, error) {
diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go
index ae73181..3071577 100644
--- a/cmd/saml2aws/main.go
+++ b/cmd/saml2aws/main.go
@@ -17,7 +17,7 @@ var (
 	Version = "1.0.0"
 )
 
-// The `cmdLineList` type is used to make a `[]string` meet the requiements
+// The `cmdLineList` type is used to make a `[]string` meet the requirements
 // of the kingpin.Value interface
 type cmdLineList []string
 
@@ -89,8 +89,10 @@ func main() {
 	execFlags := new(flags.LoginExecFlags)
 	execFlags.CommonFlags = commonFlags
 	cmdExec.Flag("profile", "The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE)").Envar("SAML2AWS_PROFILE").Short('p').StringVar(&commonFlags.Profile)
+	cmdExec.Flag("exec-profile", "The AWS profile to utilize for command execution. Useful to allow the aws cli to perform secondary role assumption. (env: SAML2AWS_EXEC_PROFILE)").Envar("SAML2AWS_EXEC_PROFILE").StringVar(&execFlags.ExecProfile)
 	cmdLine := buildCmdList(cmdExec.Arg("command", "The command to execute."))
 
+
 	// `list` command and settings
 	cmdListRoles := app.Command("list-roles", "List available role ARNs.")
 	listRolesFlags := new(flags.LoginExecFlags)
@@ -147,3 +149,4 @@ func main() {
 		os.Exit(1)
 	}
 }
+
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 4ce4c1e..b0df484 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -32,6 +32,7 @@ type LoginExecFlags struct {
 	CommonFlags  *CommonFlags
 	Force        bool
 	DuoMFAOption string
+	ExecProfile  string
 }
 
 // ApplyFlagOverrides overrides IDPAccount with command line settings
diff --git a/pkg/shell/env.go b/pkg/shell/env.go
index 264f749..6f78f1e 100644
--- a/pkg/shell/env.go
+++ b/pkg/shell/env.go
@@ -2,20 +2,29 @@ package shell
 
 import (
 	"fmt"
-
+	"github.com/versent/saml2aws/pkg/flags"
 	"github.com/versent/saml2aws/pkg/awsconfig"
 	"github.com/versent/saml2aws/pkg/cfg"
 )
 
 // BuildEnvVars build an array of env vars in the format required for exec
-func BuildEnvVars(awsCreds *awsconfig.AWSCredentials, account *cfg.IDPAccount) []string {
-	return []string{
-		fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", awsCreds.AWSAccessKey),
-		fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", awsCreds.AWSSecretKey),
+func BuildEnvVars(awsCreds *awsconfig.AWSCredentials, account *cfg.IDPAccount, execFlags *flags.LoginExecFlags) []string {
+
+	environmentVars := []string  {
 		fmt.Sprintf("AWS_SESSION_TOKEN=%s", awsCreds.AWSSessionToken),
 		fmt.Sprintf("AWS_SECURITY_TOKEN=%s", awsCreds.AWSSecurityToken),
 		fmt.Sprintf("EC2_SECURITY_TOKEN=%s", awsCreds.AWSSecurityToken),
-		fmt.Sprintf("AWS_PROFILE=%s", account.Profile),
-		fmt.Sprintf("AWS_DEFAULT_PROFILE=%s", account.Profile),
 	}
+
+	//To run exec with a non default (saml) profile 'AWS_ACCESS_KEY_ID' and 'AWS_SECRET_ACCESS_KEY' must not be set
+	if execFlags.ExecProfile == "" {
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", awsCreds.AWSAccessKey))
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", awsCreds.AWSSecretKey))
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_PROFILE=%s", account.Profile))
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_DEFAULT_PROFILE=%s", account.Profile))
+	} else {
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_PROFILE=%s", execFlags.ExecProfile))
+		environmentVars = append(environmentVars, fmt.Sprintf("AWS_DEFAULT_PROFILE=%s", execFlags.ExecProfile))
+	}
+	return environmentVars
 }


Test patch:
diff --git a/pkg/shell/env_test.go b/pkg/shell/env_test.go
index dca8108..07d208a 100644
--- a/pkg/shell/env_test.go
+++ b/pkg/shell/env_test.go
@@ -6,16 +6,17 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/versent/saml2aws/pkg/awsconfig"
 	"github.com/versent/saml2aws/pkg/cfg"
+	"github.com/versent/saml2aws/pkg/flags"
 )
 
 func TestBuildEnvVars(t *testing.T) {
 
 	expectedArray := []string{
-		"AWS_ACCESS_KEY_ID=123",
-		"AWS_SECRET_ACCESS_KEY=345",
 		"AWS_SESSION_TOKEN=567",
 		"AWS_SECURITY_TOKEN=567",
 		"EC2_SECURITY_TOKEN=567",
+		"AWS_ACCESS_KEY_ID=123",
+		"AWS_SECRET_ACCESS_KEY=345",
 		"AWS_PROFILE=saml",
 		"AWS_DEFAULT_PROFILE=saml",
 	}
@@ -31,5 +32,7 @@ func TestBuildEnvVars(t *testing.T) {
 		AWSSessionToken:  "567",
 	}
 
-	assert.Equal(t, expectedArray, BuildEnvVars(awsCreds, account))
+	flags := new(flags.LoginExecFlags)
+
+	assert.Equal(t, expectedArray, BuildEnvVars(awsCreds, account, flags))
 }