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

feat: Include session expiration as AWS_SESSION_EXPIRATION #48

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ bin/assume-role: *.go
bin: bin/assume-role-Linux bin/assume-role-Darwin bin/assume-role-Windows.exe

bin/assume-role-Linux: *.go
env GOOS=linux go build -o $@ .
docker run --env GOOS=linux --volume ${PWD}:/go/assume-role --workdir /go/assume-role golang:1.13 go build -mod=vendor -o ./bin/assume-role-Linux
bin/assume-role-Darwin: *.go
env GOOS=darwin go build -o $@ .
docker run --env GOOS=darwin --volume ${PWD}:/go/assume-role --workdir /go/assume-role golang:1.13 go build -mod=vendor -o ./bin/assume-role-Darwin
bin/assume-role-Windows.exe: *.go
env GOOS=windows go build -o $@ .
docker run --env GOOS=windows --volume ${PWD}:/go/assume-role --workdir /go/assume-role golang:1.13 go build -mod=vendor -o ./bin/assume-role-Windows.exe

clean:
rm -rf bin/*

test:
go test -race $(shell go list ./... | grep -v /vendor/)
docker run --env TZ=America/New_York --volume ${PWD}:/go/assume-role --workdir /go/assume-role golang:1.13 go test -mod=vendor -v .
97 changes: 49 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,14 @@ This tool will request and set temporary credentials in your shell environment v

## Installation

On OS X, the best way to get it is to use homebrew:

```bash
brew install remind101/formulae/assume-role
To install version 1.0.0:
```

If you have a working Go 1.6/1.7 environment:

```bash
$ go get -u github.com/remind101/assume-role
```

On Windows with PowerShell, you can use [scoop.sh](http://scoop.sh/)

```cmd
$ scoop bucket add extras
$ scoop install assume-role
cd /tmp/
curl -L -o assume-role https://github.com/roadtrippers/assume-role/releases/download/1.0.0/assume-role-Darwin
echo 'dff2c8219d8f1ccf4574a5537bd04bf1c9f70f032d243e411b9f3ba724deead4 ./assume-role' > ./assume-role.sha256
shasum -c assume-role.sha256 || echo "DO NOT PROCEED. SHASUM DID NOT MATCH"
mv ./assume-role /usr/local/bin
chmod +x /usr/local/bin/assume-role
```

## Configuration
Expand Down Expand Up @@ -86,41 +77,51 @@ $ assume-role prod aws iam get-user
MFA code: 123456
```

If no command is provided, `assume-role` will output the temporary security credentials:

```bash
$ assume-role prod
export AWS_ACCESS_KEY_ID="ASIAI....UOCA"
export AWS_SECRET_ACCESS_KEY="DuH...G1d"
export AWS_SESSION_TOKEN="AQ...1BQ=="
export AWS_SECURITY_TOKEN="AQ...1BQ=="
export ASSUMED_ROLE="prod"
# Run this to configure your shell:
# eval $(assume-role prod)
Useful bash profile setup:
```

Or windows PowerShell:
```cmd
$env:AWS_ACCESS_KEY_ID="ASIAI....UOCA"
$env:AWS_SECRET_ACCESS_KEY="DuH...G1d"
$env:AWS_SESSION_TOKEN="AQ...1BQ=="
$env:AWS_SECURITY_TOKEN="AQ...1BQ=="
$env:ASSUMED_ROLE="prod"
# Run this to configure your shell:
# assume-role.exe prod | Invoke-Expression
function assume-role(){
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_SECURITY_TOKEN
unset ASSUMED_ROLE
eval $(/usr/local/bin/assume-role $@)
export TF_VAR_aws_access_key=${AWS_ACCESS_KEY_ID}
export TF_VAR_aws_secret_key=${AWS_SECRET_ACCESS_KEY}
export TF_VAR_aws_session_token=${AWS_SESSION_TOKEN}
export AWS_ACCESS_KEY=${TF_VAR_aws_access_key}
export AWS_SECRET_KEY=${TF_VAR_aws_secret_key}
}

print_assumed_role(){
if test ! -z "${ASSUMED_ROLE}"; then
echo -n '['
echo -n $ASSUMED_ROLE


if test ! -z "${AWS_SESSION_EXPIRATION}"; then
echo -n ", ${AWS_SESSION_EXPIRATION}"
fi

echo -n '] '
fi
}

setup_default_prompt(){

if [ ! -z "${PROMPT_COMMAND}" ]; then
PROMPT_COMMAND="${PROMPT_COMMAND} ;"
fi
export PROMPT_COMMAND="${PROMPT_COMMAND} print_assumed_role"
}

# at the end of ~/.bash_profile
setup_default_prompt
```

If you use `eval $(assume-role)` frequently, you may want to create a alias for it:
This will make bash prompt to show assumed role configuration
and its expiration time, as shown below:

* zsh
```shell
alias assume-role='function(){eval $(command assume-role $@);}'
```
* bash
```shell
function assume-role { eval $( $(which assume-role) $@); }
[vpc-thlprod, Wed 17:41] YOUR_REGULAR_BASH_PROMPT
```

## TODO

* [ ] Cache credentials.
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/remind101/assume-role

go 1.13

require (
github.com/aws/aws-sdk-go v1.26.3
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
gopkg.in/yaml.v2 v2.2.7
)
22 changes: 22 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
github.com/aws/aws-sdk-go v1.26.3 h1:szQdfJcUBAhQT0zZEx4sxoDuWb7iScoucxCiVxDmaBk=
github.com/aws/aws-sdk-go v1.26.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
51 changes: 34 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ func main() {
// Load credentials from configFilePath if it exists, else use regular AWS config
var creds *credentials.Value
var err error
var expiration *time.Time
if roleArnRe.MatchString(role) {
creds, err = assumeRole(role, "", *duration)
creds, expiration, err = assumeRole(role, "", *duration)
} else if _, err = os.Stat(configFilePath); err == nil {
fmt.Fprintf(os.Stderr, "WARNING: using deprecated role file (%s), switch to config file"+
" (https://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html)\n",
Expand All @@ -86,21 +87,21 @@ func main() {
must(fmt.Errorf("%s not in %s", role, configFilePath))
}

creds, err = assumeRole(roleConfig.Role, roleConfig.MFA, *duration)
creds, expiration, err = assumeRole(roleConfig.Role, roleConfig.MFA, *duration)
} else {
creds, err = assumeProfile(role)
creds, expiration, err = assumeProfile(role)
}

must(err)

if len(args) == 0 {
switch *format {
case "powershell":
printPowerShellCredentials(role, creds)
printPowerShellCredentials(role, creds, expiration)
case "bash":
printCredentials(role, creds)
printCredentials(role, creds, expiration)
case "fish":
printFishCredentials(role, creds)
printFishCredentials(role, creds, expiration)
default:
flag.Usage()
os.Exit(1)
Expand Down Expand Up @@ -128,46 +129,59 @@ func execWithCredentials(role string, argv []string, creds *credentials.Value) e
return syscall.Exec(argv0, argv, env)
}

// given that max time for a asession role is 12 hours
// just show DayOfWeek HH:MM, example: "Mon 12:01" in the local time zone
func formatExpirationTime(t *time.Time) string {
if t == nil {
return ""
}
l := t.Local()
return fmt.Sprintf("%s %02d:%02d", l.Weekday().String()[0:3], l.Hour(), l.Minute())
}

// printCredentials prints the credentials in a way that can easily be sourced
// with bash.
func printCredentials(role string, creds *credentials.Value) {
func printCredentials(role string, creds *credentials.Value, expiration *time.Time) {
fmt.Printf("export AWS_ACCESS_KEY_ID=\"%s\"\n", creds.AccessKeyID)
fmt.Printf("export AWS_SECRET_ACCESS_KEY=\"%s\"\n", creds.SecretAccessKey)
fmt.Printf("export AWS_SESSION_TOKEN=\"%s\"\n", creds.SessionToken)
fmt.Printf("export AWS_SECURITY_TOKEN=\"%s\"\n", creds.SessionToken)
fmt.Printf("export ASSUMED_ROLE=\"%s\"\n", role)
fmt.Printf("export AWS_SESSION_EXPIRATION=\"%s\"\n", formatExpirationTime(expiration))
fmt.Printf("# Run this to configure your shell:\n")
fmt.Printf("# eval $(%s)\n", strings.Join(os.Args, " "))
}

// printFishCredentials prints the credentials in a way that can easily be sourced
// with fish.
func printFishCredentials(role string, creds *credentials.Value) {
func printFishCredentials(role string, creds *credentials.Value, expiration *time.Time) {
fmt.Printf("set -gx AWS_ACCESS_KEY_ID \"%s\";\n", creds.AccessKeyID)
fmt.Printf("set -gx AWS_SECRET_ACCESS_KEY \"%s\";\n", creds.SecretAccessKey)
fmt.Printf("set -gx AWS_SESSION_TOKEN \"%s\";\n", creds.SessionToken)
fmt.Printf("set -gx AWS_SECURITY_TOKEN \"%s\";\n", creds.SessionToken)
fmt.Printf("set -gx ASSUMED_ROLE \"%s\";\n", role)
fmt.Printf("set -gx AWS_SESSION_EXPIRATION \"%s\"", formatExpirationTime(expiration))
fmt.Printf("# Run this to configure your shell:\n")
fmt.Printf("# eval (%s)\n", strings.Join(os.Args, " "))
}

// printPowerShellCredentials prints the credentials in a way that can easily be sourced
// with Windows powershell using Invoke-Expression.
func printPowerShellCredentials(role string, creds *credentials.Value) {
func printPowerShellCredentials(role string, creds *credentials.Value, expiration *time.Time) {
fmt.Printf("$env:AWS_ACCESS_KEY_ID=\"%s\"\n", creds.AccessKeyID)
fmt.Printf("$env:AWS_SECRET_ACCESS_KEY=\"%s\"\n", creds.SecretAccessKey)
fmt.Printf("$env:AWS_SESSION_TOKEN=\"%s\"\n", creds.SessionToken)
fmt.Printf("$env:AWS_SECURITY_TOKEN=\"%s\"\n", creds.SessionToken)
fmt.Printf("$env:ASSUMED_ROLE=\"%s\"\n", role)
fmt.Printf("$env:AWS_SESSION_EXPIRATION=\"%s\"\n", formatExpirationTime(expiration))
fmt.Printf("# Run this to configure your shell:\n")
fmt.Printf("# %s | Invoke-Expression \n", strings.Join(os.Args, " "))
}

// assumeProfile assumes the named profile which must exist in ~/.aws/config
// (https://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html) and returns the temporary STS
// credentials.
func assumeProfile(profile string) (*credentials.Value, error) {
func assumeProfile(profile string) (*credentials.Value, *time.Time, error) {
sess := session.Must(session.NewSessionWithOptions(session.Options{
Profile: profile,
SharedConfigState: session.SharedConfigEnable,
Expand All @@ -176,13 +190,17 @@ func assumeProfile(profile string) (*credentials.Value, error) {

creds, err := sess.Config.Credentials.Get()
if err != nil {
return nil, err
return nil, nil, err
}
return &creds, nil
expiration, err := sess.Config.Credentials.ExpiresAt()
if err != nil {
return nil, nil, err
}
return &creds, &expiration, nil
}

// assumeRole assumes the given role and returns the temporary STS credentials.
func assumeRole(role, mfa string, duration time.Duration) (*credentials.Value, error) {
func assumeRole(role, mfa string, duration time.Duration) (*credentials.Value, *time.Time, error) {
sess := session.Must(session.NewSession())

svc := sts.New(sess)
Expand All @@ -196,23 +214,22 @@ func assumeRole(role, mfa string, duration time.Duration) (*credentials.Value, e
params.SerialNumber = aws.String(mfa)
token, err := readTokenCode()
if err != nil {
return nil, err
return nil, nil, err
}
params.TokenCode = aws.String(token)
}

resp, err := svc.AssumeRole(params)

if err != nil {
return nil, err
return nil, nil, err
}

var creds credentials.Value
creds.AccessKeyID = *resp.Credentials.AccessKeyId
creds.SecretAccessKey = *resp.Credentials.SecretAccessKey
creds.SessionToken = *resp.Credentials.SessionToken

return &creds, nil
return &creds, resp.Credentials.Expiration, nil
}

type roleConfig struct {
Expand Down
51 changes: 51 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"testing"
"time"
)

func parseTime(v string) *time.Time {
t, _ := time.Parse(time.RFC1123Z, v)
return &t
}

func TestFormatExpirationTime(t *testing.T) {

var testCases = []struct {
in *time.Time
out string
description string
}{
{
parseTime("Tue, 17 Dec 2019 12:25:28 -0500"),
"Tue 12:25",
"Test base case",
},
{
parseTime("Tue, 17 Dec 2019 17:25:28 -0000"),
"Tue 12:25",
"Test time in UTC",
},
{
parseTime("Tue, 17 Dec 2019 18:25:28 -0000"),
"Tue 13:25",
"Test time in after 1:00 PM UTC",
},
{
nil,
"",
"Test when input is nil",
},
}

for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
actualOutput := formatExpirationTime(tt.in)
if actualOutput != tt.out {
t.Errorf("got %v, want %v", actualOutput, tt.out)
}
})
}

}
Loading