├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── aliases.go ├── aliases_test.go ├── config.go ├── config_test.go ├── example ├── bash_aliases.sh └── config.yaml ├── go.mod ├── go.sum ├── printer.go ├── printer_test.go ├── profile_writer.go ├── profile_writer_test.go ├── swamp.go └── swamp_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.16 20 | 21 | - name: Build 22 | run: make -j4 test build 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .idea/ 17 | build 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## swamp v0.14.0 4 | 5 | Breaking changes: 6 | 7 | * `-instance`: removed. 8 | 9 | ## swamp v0.13.0 10 | 11 | * shrink binaries by updating to aws-sdk-2 12 | 13 | ## swamp v0.12.0 14 | 15 | * `-alias-config`: generate lowercase profile names and aliases 16 | * `-target-account` and `-target-role`: allow empty parameters to create only intermediate/session profile 17 | 18 | Breaking changes: 19 | 20 | * `-export-profile` and `-export-file`: removed. Please use `-exec` instead, e.g. `-exec bash` 21 | 22 | ## swamp v0.11.0 23 | 24 | * `-alias-config`: support roles with `/` 25 | 26 | ## swamp v0.10.0 27 | 28 | * `-alias-config` generates shell aliases 29 | 30 | ## swamp v0.9.1 31 | 32 | * fix finding aws credentials on windows 33 | 34 | ## swamp v0.9 35 | 36 | * `-quiet` suppresses output 37 | * more explicit error messages for windows users 38 | 39 | ## swamp v0.8.2 40 | 41 | * fix string escaping 42 | 43 | ## swamp v0.8.1 44 | 45 | * add details to error messages 46 | 47 | ## swamp v0.8 48 | 49 | * `-exec` allows executing command with the target role assumed 50 | * write current default region to all profiles 51 | 52 | ## swamp v0.7 53 | 54 | * `-mfa-exec` allows to automatically obtain the mfa-device token 55 | * `-target-profile` defaults to `swamp` 56 | * `-profile` defaults to `` 57 | * `-region` defaults to `` 58 | * `-instance` is deprecated and replaced by no-op 59 | * obey environment variable `AWS_SHARED_CREDENTIALS_FILE` 60 | * hide flags `-export-profile` and `-export-file` on windows 61 | 62 | ## swamp v0.6 63 | 64 | * add locking for credentials file (#6) 65 | 66 | ## swamp v0.5.2 67 | 68 | * enable cgo for default linux build 69 | * disable cgo for alpine linux build 70 | 71 | ## swamp v0.5.1 72 | 73 | * disable cgo for linux build 74 | 75 | ## swamp v0.5 76 | 77 | * Support windows 78 | * Strip blanks from mfa input 79 | 80 | ## swamp v0.4 81 | 82 | * Use existing session name for new sessions 83 | * Write file for easy setting target profile in your shell (#5) 84 | 85 | ## swamp v0.3 86 | 87 | * Allow setting `--target-role` as `arn` (#4) 88 | 89 | ## swamp v0.2 90 | 91 | * Allow using instance profiles on ec2 and ecs 92 | 93 | ## swamp v0.1 94 | 95 | * Initial release 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please fork and send a pull request. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Felix Bechstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build clean test 2 | 3 | BIN_DIR := build 4 | BIN_NAME := swamp 5 | CGO0_BINS := $(BIN_DIR)/$(BIN_NAME)-alpine-amd64 $(BIN_DIR)/$(BIN_NAME)-darwin-amd64 $(BIN_DIR)/$(BIN_NAME)-windows-amd64.exe 6 | CGO1_BINS := $(BIN_DIR)/$(BIN_NAME)-linux-amd64 7 | LOCAL_BIN := $(BIN_DIR)/$(BIN_NAME) 8 | BINS := $(CGO0_BINS) $(CGO1_BINS) $(LOCAL_BIN) 9 | TARGET ?= $(HOME)/bin 10 | VERSION=$(shell git describe --tags) 11 | 12 | temp=$(subst -, ,$@) 13 | os=$(subst alpine,linux,$(word 2, $(temp))) 14 | arch=$(subst .exe,,$(word 3, $(temp))) 15 | 16 | all: test build 17 | 18 | build: $(BINS) 19 | 20 | install: $(LOCAL_BIN) 21 | cp $(LOCAL_BIN) $(TARGET)/ 22 | 23 | clean: 24 | rm -rf $(BIN_DIR) 25 | 26 | test: 27 | go test -v -cover ./... 28 | 29 | $(CGO0_BINS): *.go 30 | GOOS=$(os) GOARCH=$(arch) CGO_ENABLED=0 go build -o '$@' *.go 31 | 32 | $(CGO1_BINS): *.go 33 | GOOS=$(os) GOARCH=$(arch) CGO_ENABLED=1 go build -o '$@' *.go 34 | 35 | $(LOCAL_BIN): *.go 36 | go build -o '$@' *.go 37 | 38 | fmt: *.go 39 | go fmt *.go 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SWAMP: Profile Manager for AWS [![build](https://github.com/felixb/swamp/actions/workflows/build.yml/badge.svg)](https://github.com/felixb/swamp/actions/workflows/build.yml) 2 | 3 | You can use `swamp` to switch AWS profiles with ease. 4 | 5 | ## Use case 6 | 7 | `swamp` assumes you have an AWS account with CLI access credentials and you want to assume role into a set of AWS accounts from there. 8 | `swamp` optionally supports MFA authentication before assuming the target role. 9 | 10 | ### Without MFA 11 | 12 | `swamp` calls `aws sts assume-role` and writes the returned credentials into the specified target profile. 13 | 14 | #### Example: 15 | 16 | Create a session token based on your default profile: 17 | 18 | ``` 19 | $ swamp -profile default -target-profile target -target-role admin -account [target-account-id] 20 | Wrote session token for profile target 21 | Token is valid until: 2017-07-06 08:31:10 +0000 UTC 22 | ``` 23 | 24 | Create a session token based on your instance profile when running in an ec2 instance or ecs task: 25 | 26 | ``` 27 | $ swamp -instance -target-profile target -target-role admin -account [target-account-id] 28 | Wrote session token for profile target 29 | Token is valid until: 2017-07-06 08:31:10 +0000 UTC 30 | ``` 31 | 32 | ### With MFA 33 | 34 | `swamp` calls `aws sts get-session-token` with MFA authentication to obtain a profile with enabled MFA. The returned credentials are written to the specified intermediate profile. 35 | Subsequent calls may skip that step as long as the session token is still valid. 36 | With these intermediate credentials `aws sts assume-role` is called as above. 37 | 38 | #### Example: 39 | 40 | ``` 41 | $ swamp -target-profile target -target-role admin -account [target-account-id] -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] 42 | Enter mfa token for arn:aws:iam::[origin-account-id]:mfa/[userid]: XXXXXX 43 | Wrote session token for profile session-token 44 | Token is valid until: 2017-07-06 20:32:09 +0000 UTC 45 | Wrote session token for profile target 46 | Token is valid until: 2017-07-06 08:31:10 +0000 UTC 47 | ``` 48 | 49 | And run it again: 50 | 51 | ``` 52 | $ swamp -target-profile target -target-role admin -account [target-account-id] -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] 53 | Session token for profile session-token is still valid 54 | Wrote session token for profile target 55 | Token is valid until: 2017-07-06 08:32:15 +0000 UTC 56 | ``` 57 | 58 | Or create a session profile only: 59 | 60 | ``` 61 | $ swamp -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] 62 | Enter mfa token for arn:aws:iam::[origin-account-id]:mfa/[userid]: XXXXXX 63 | Wrote session token for profile session-token 64 | Token is valid until: 2017-07-06 20:32:09 +0000 UTC 65 | ``` 66 | 67 | ### Auto-Obtain MFA Token 68 | 69 | If using swamp with an mfa-enabled account you can use the `-mfa-exec` flag to tell swamp to try to obtain the token itself. 70 | You need to give an executable command which returns the 6-digit code. 71 | 72 | swamp is known to integrate well with the following tools: 73 | 74 | * [pass](https://www.passwordstore.org/) / [pass-otp](https://github.com/tadfisher/pass-otp): `-mfa-exec "pass otp amazonaws.com"` 75 | * [ykman](https://developers.yubico.com/yubikey-manager/): `-mfa-exec "ykman oath code amazonaws.com | awk '{ print $NF }'"` 76 | 77 | #### Example: 78 | 79 | ``` 80 | $ swamp -target-profile target -target-role admin -account [target-account-id] -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] -mfa-exec "pass otp amazonaws.com" 81 | Obtaining mfa token for: arn:aws:iam::[origin-account-id]:mfa/[userid] 82 | Wrote session token for profile session-token 83 | Token is valid until: 2017-07-06 20:32:09 +0000 UTC 84 | Wrote session token for profile target 85 | Token is valid until: 2017-07-06 08:31:10 +0000 UTC 86 | ``` 87 | 88 | ### Renew 89 | 90 | `swamp` allows running in a loop to create a new profile for the target account before credentials expire. 91 | It even works with enabled MFA thanks to the cached intermediate credentials. 92 | 93 | #### Example 94 | 95 | ``` 96 | $ swamp -target-profile target -target-role admin -account [target-account-id] -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] -renew 97 | Enter mfa token for arn:aws:iam::[origin-account-id]:mfa/[userid]: XXXXXX 98 | Wrote session token for profile session-token 99 | Token is valid until: 2017-07-06 20:32:09 +0000 UTC 100 | Wrote session token for profile target 101 | Token is valid until: 2017-07-06 08:31:10 +0000 UTC 102 | Session token for profile session-token is still valid 103 | Wrote session token for profile target 104 | Token is valid until: 2017-07-06 08:46:10 +0000 UTC 105 | ... 106 | ``` 107 | 108 | ### Set profile in environment 109 | To get a shell with `AWS_PROFILE` properly set, just use the `-exec` flag and run the shell of your choice. Close the shell when done and you are back int the context before running swamp. 110 | 111 | #### Example 112 | ``` 113 | $ echo "outer shell: '$AWS_PROFILE'" 114 | outer shell: '' 115 | $ swamp -target-profile target -target-role admin -account [target-account-id] -mfa-device arn:aws:iam::[origin-account-id]:mfa/[userid] -exec bash 116 | $ echo "inner shell: '$AWS_PROFILE'" 117 | inner shell: 'target' 118 | $ exit 119 | $ echo "outer shell: '$AWS_PROFILE'" 120 | outer shell: '' 121 | ``` 122 | 123 | ### Generating shell aliases 124 | `swamp` has a lot of command line options. It is strongly recommended to create some kind of aliases for running swamp more easily. 125 | `swamp -alias-config ` does exactly that: 126 | ``` 127 | swamp -alias-config example/config.yaml >> ~/.bashrc 128 | ``` 129 | The output `example/bash_aliases.sh` file is generated from the example config `example/config.yaml`. 130 | 131 | 132 | ## Install 133 | 134 | ### General 135 | Fetch the latest binary from https://github.com/felixb/swamp/releases. 136 | You may install it from source by running `make install` optionally setting something like `TARGET=/usr/local/bin/` to specify a different installation target. 137 | 138 | ### macOS 139 | You can install swamp on macOS using [brew](https://brew.sh/) with a third-party repository. Simply run `brew tap splieth/swamp` to add the repository and then `brew install swamp` to install the binary. 140 | -------------------------------------------------------------------------------- /aliases.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v2" 6 | "html/template" 7 | "io" 8 | "io/ioutil" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | type aliasConfig struct { 15 | AllArgs string `yaml:"allArgs"` 16 | AllExecs map[string]string `yaml:"allExecs"` 17 | DefaultAdditionalArgs string `yaml:"defaultAdditionalArgs"` 18 | Teams []team `yaml:"teams"` 19 | } 20 | 21 | type team struct { 22 | Name string `yaml:"name"` 23 | AdditionalArgs string `yaml:"additionalArgs"` 24 | Accounts []account `yaml:"accounts"` 25 | } 26 | 27 | type account struct { 28 | AccountId string `yaml:"accountId"` 29 | Name string `yaml:"name"` 30 | Roles []string `yaml:"roles"` 31 | Execs map[string]string `yaml:"execs"` 32 | } 33 | 34 | type templateVars struct { 35 | AccountId string 36 | AccountName string 37 | AliasName string 38 | Args template.HTML 39 | ProfileName string 40 | Role string 41 | TeamName string 42 | } 43 | 44 | const ( 45 | aliasTemplate = ` 46 | function swamp-{{.AliasName}}() { 47 | SWAMP_TARGET_PROFILE='{{.ProfileName}}' \ 48 | SWAMP_ACCOUNT='{{.AccountId}}' \ 49 | SWAMP_ACCOUNT_NAME='{{.AccountName}}' \ 50 | SWAMP_TARGET_ROLE='{{.Role}}' \ 51 | swamp {{.Args}} 52 | } 53 | ` 54 | ) 55 | 56 | func generateAliases(w io.Writer, path string) error { 57 | fmt.Fprintln(w, "# This aliases are generated with swamp") 58 | 59 | c := &aliasConfig{} 60 | if bytes, err := ioutil.ReadFile(path); err != nil { 61 | return err 62 | } else { 63 | if err := yaml.Unmarshal(bytes, c); err != nil { 64 | return err 65 | } 66 | 67 | for _, team := range c.Teams { 68 | if err := generateAliasTeam(w, c, team); err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func generateAliasTeam(w io.Writer, config *aliasConfig, team team) error { 77 | for _, account := range team.Accounts { 78 | if err := generateAliasAccount(w, config, team, account); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func generateAliasAccount(w io.Writer, config *aliasConfig, team team, account account) error { 86 | if tpl, err := template.New("aliases").Option("missingkey=error").Parse(aliasTemplate); err != nil { 87 | return err 88 | } else { 89 | for _, role := range account.Roles { 90 | generateAliasRole(w, config, team, account, role, tpl) 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func generateAliasRole(w io.Writer, config *aliasConfig, team team, account account, role string, tpl *template.Template) { 97 | re := regexp.MustCompile(`[^a-zA-Z0-9_]`) 98 | profileName := strings.ToLower(team.Name + "-" + account.Name + "-" + re.ReplaceAllString(role, "-")) 99 | args := config.AllArgs 100 | if team.AdditionalArgs != "" { 101 | args += " " + team.AdditionalArgs 102 | } else { 103 | args += " " + config.DefaultAdditionalArgs 104 | } 105 | args += " -account '" + account.AccountId + "'" 106 | args += " -target-role '" + role + "'" 107 | args += " -target-profile '" + profileName + "'" 108 | 109 | baseArgs := args 110 | 111 | t := templateVars{ 112 | AccountId: account.AccountId, 113 | AccountName: account.Name, 114 | AliasName: profileName, 115 | Args: template.HTML(baseArgs + ` "${@}"`), 116 | ProfileName: profileName, 117 | Role: role, 118 | TeamName: team.Name, 119 | } 120 | tpl.Execute(w, t) 121 | generateExecs(w, baseArgs, profileName, tpl, t, config.AllExecs) 122 | generateExecs(w, baseArgs, profileName, tpl, t, account.Execs) 123 | } 124 | 125 | func generateExecs(w io.Writer, baseArgs string, profileName string, tpl *template.Template, t templateVars, execs map[string]string) { 126 | var keys []string 127 | for k := range execs { 128 | keys = append(keys, k) 129 | } 130 | sort.Strings(keys) 131 | 132 | for _, k := range keys { 133 | t.AliasName = profileName + "-" + k 134 | t.Args = template.HTML(baseArgs + ` -exec "` + execs[k] + `"`) 135 | tpl.Execute(w, t) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /aliases_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "io" 7 | "io/ioutil" 8 | "testing" 9 | ) 10 | 11 | func TestAliases_Generate(t *testing.T) { 12 | r, w := io.Pipe() 13 | 14 | go func() { 15 | generateAliases(w, "example/config.yaml") 16 | w.Close() 17 | }() 18 | 19 | expected, e := ioutil.ReadFile("example/bash_aliases.sh") 20 | assert.NoError(t, e) 21 | expectedString := string(expected) 22 | 23 | buf := new(bytes.Buffer) 24 | _, e = buf.ReadFrom(r) 25 | assert.NoError(t, e) 26 | actualString := buf.String() 27 | 28 | assert.Equal(t, len(expected), len(actualString)) 29 | assert.Equal(t, expectedString, actualString) 30 | } 31 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | INTERMEDIATE_SESSION_TOKEN_DURATION = int64(12 * 60 * 60) 14 | TARGET_SESSION_TOKEN_DURATION = int64(60 * 60) 15 | VERSION = "0.14.0" 16 | ) 17 | 18 | type SwampConfig struct { 19 | aliasConfig string 20 | targetAccount string 21 | intermediateProfile string 22 | intermediateDuration int64 23 | targetProfile string 24 | targetRole string 25 | targetDuration int64 26 | profile string 27 | region string 28 | tokenSerialNumber string 29 | useInstanceProfile bool 30 | renew bool 31 | exec string 32 | mfaExec string 33 | quiet bool 34 | } 35 | 36 | func NewSwampConfig() *SwampConfig { 37 | return &SwampConfig{ 38 | aliasConfig: "", 39 | targetAccount: "", 40 | intermediateProfile: "session-token", 41 | intermediateDuration: INTERMEDIATE_SESSION_TOKEN_DURATION, 42 | targetProfile: "swamp", 43 | targetRole: "", 44 | targetDuration: TARGET_SESSION_TOKEN_DURATION, 45 | profile: "", 46 | region: "", 47 | tokenSerialNumber: "", 48 | renew: false, 49 | exec: "", 50 | mfaExec: "", 51 | quiet: false, 52 | } 53 | } 54 | 55 | func (config *SwampConfig) isRoleArn() bool { 56 | return strings.HasPrefix(config.targetRole, "arn:aws:iam::") 57 | } 58 | 59 | func (config *SwampConfig) GetRoleArn() *string { 60 | if config.isRoleArn() { 61 | return &config.targetRole 62 | } else { 63 | arn := fmt.Sprintf("arn:aws:iam::%s:role/%s", config.targetAccount, config.targetRole) 64 | return &arn 65 | } 66 | } 67 | 68 | func (config *SwampConfig) SetupFlags() { 69 | flag.StringVar(&config.targetAccount, "account", config.targetAccount, "AWS account") 70 | flag.StringVar(&config.intermediateProfile, "intermediate-profile", config.intermediateProfile, "Intermediate AWS CLI profile") 71 | flag.Int64Var(&config.intermediateDuration, "intermediate-duration", config.intermediateDuration, "Token duration in seconds for intermediate profile") 72 | flag.StringVar(&config.targetProfile, "target-profile", config.targetProfile, "Write this AWS CLI profile") 73 | flag.StringVar(&config.targetRole, "target-role", config.targetRole, "AWS role to assume (can either be ARN or name)") 74 | flag.Int64Var(&config.targetDuration, "target-duration", config.targetDuration, "Token duration in seconds for target profile") 75 | flag.StringVar(&config.profile, "profile", config.profile, "AWS CLI profile") 76 | flag.StringVar(&config.region, "region", config.region, "AWS region") 77 | flag.StringVar(&config.tokenSerialNumber, "mfa-device", config.tokenSerialNumber, "MFA device arn") 78 | flag.BoolVar(&config.renew, "renew", config.renew, "Renew token every duration/2") 79 | flag.BoolVar(&config.quiet, "quiet", config.quiet, "Suppress output") 80 | if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { 81 | // platform specific flags 82 | flag.StringVar(&config.aliasConfig, "alias-config", config.aliasConfig, "Generate aliases from yaml `file`") 83 | flag.StringVar(&config.exec, "exec", config.exec, "Execute this commend with AWS_PROFILE set to target protile") 84 | flag.StringVar(&config.mfaExec, "mfa-exec", config.mfaExec, "Executable command for obtaining mfa-device token") 85 | } 86 | flag.Usage = flagUsage 87 | } 88 | 89 | func (config *SwampConfig) validateDefaultFlags() error { 90 | if config.targetRole != "" || config.tokenSerialNumber == "" { 91 | if err := checkStringFlagNotEmpty("target-profile", config.targetProfile); err != nil { 92 | return err 93 | } 94 | if !config.isRoleArn() { 95 | if err := checkStringFlagNotEmpty("account", config.targetAccount); err != nil { 96 | return err 97 | } 98 | } else { 99 | if config.targetAccount != "" { 100 | return errors.New("Target role in ARN format and target account are mutual exclusive") 101 | } 102 | } 103 | } 104 | 105 | if config.tokenSerialNumber != "" { 106 | if err := checkStringFlagNotEmpty("intermediate-profile", config.intermediateProfile); err != nil { 107 | return err 108 | } 109 | } 110 | 111 | if config.mfaExec != "" { 112 | if err := checkStringFlagNotEmpty("mfa-device", config.tokenSerialNumber); err != nil { 113 | return err 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func (config *SwampConfig) validateAliasFlags() error { 121 | if _, err := os.Stat(config.aliasConfig); os.IsNotExist(err) { 122 | return err 123 | } 124 | return nil 125 | } 126 | 127 | func (config *SwampConfig) Validate() error { 128 | if config.aliasConfig == "" { 129 | return config.validateDefaultFlags() 130 | } else { 131 | return config.validateAliasFlags() 132 | } 133 | } 134 | 135 | func checkStringFlagNotEmpty(name string, f string) error { 136 | if f == "" { 137 | return fmt.Errorf("Missing mandatory parameter: %s", name) 138 | } 139 | return nil 140 | } 141 | 142 | func flagUsage() { 143 | fmt.Fprintf(os.Stderr, "Version of %s: %s\n", os.Args[0], VERSION) 144 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 145 | flag.PrintDefaults() 146 | } 147 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestSwampConfig_ValidateSessionProfileOnly(t *testing.T) { 9 | c := NewSwampConfig() 10 | c.targetAccount = "" 11 | c.targetRole = "" 12 | c.targetProfile = "" 13 | c.tokenSerialNumber = "someSerialNumber" 14 | 15 | assert.NoError(t, c.Validate()) 16 | } 17 | 18 | func TestSwampConfig_ValidateSessionProfileOnlyMissingMfaDevice(t *testing.T) { 19 | c := NewSwampConfig() 20 | c.targetAccount = "" 21 | c.targetRole = "" 22 | c.targetProfile = "" 23 | c.tokenSerialNumber = "" 24 | 25 | assert.Error(t, c.Validate()) 26 | } 27 | 28 | func TestSwampConfig_ValidateAccountAndRoleName(t *testing.T) { 29 | c := NewSwampConfig() 30 | c.targetAccount = "1234567890" 31 | c.targetRole = "some-role" 32 | 33 | assert.NoError(t, c.Validate()) 34 | } 35 | 36 | func TestSwampConfig_ValidateRoleArn(t *testing.T) { 37 | c := NewSwampConfig() 38 | c.targetAccount = "" 39 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 40 | 41 | assert.NoError(t, c.Validate()) 42 | } 43 | 44 | func TestSwampConfig_ValidateAccountAndRoleArn(t *testing.T) { 45 | c := NewSwampConfig() 46 | c.targetAccount = "1234567890" 47 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 48 | 49 | assert.Error(t, c.Validate()) 50 | } 51 | 52 | func TestSwampConfig_NotDefaults(t *testing.T) { 53 | c := NewSwampConfig() 54 | c.targetAccount = "1234567890" 55 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 56 | c.aliasConfig = "CHANGELOG.md" 57 | 58 | assert.NoError(t, c.Validate()) 59 | } 60 | 61 | func TestSwampConfig_ValidateMFADeviceAndTokenCommand(t *testing.T) { 62 | c := NewSwampConfig() 63 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 64 | c.tokenSerialNumber = "someSerialNumber" 65 | c.mfaExec = "some command" 66 | 67 | assert.NoError(t, c.Validate()) 68 | } 69 | 70 | func TestSwampConfig_ValidateNoMFADeviceButTokenCommand(t *testing.T) { 71 | c := NewSwampConfig() 72 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 73 | c.mfaExec = "some command" 74 | 75 | assert.Error(t, c.Validate()) 76 | } 77 | 78 | func TestSwampConfig_GetRoleArnWithArn(t *testing.T) { 79 | c := NewSwampConfig() 80 | c.targetRole = "arn:aws:iam::1234567890:role/some-role" 81 | c.targetAccount = "does-not-matter-at-all" 82 | 83 | arn := c.GetRoleArn() 84 | assert.Equal(t, *arn, "arn:aws:iam::1234567890:role/some-role") 85 | } 86 | 87 | func TestSwampConfig_GetRoleArnWithAccountAndRole(t *testing.T) { 88 | c := NewSwampConfig() 89 | c.targetRole = "some-role" 90 | c.targetAccount = "1234567890" 91 | 92 | arn := c.GetRoleArn() 93 | assert.Equal(t, *arn, "arn:aws:iam::1234567890:role/some-role") 94 | } 95 | 96 | func TestSwampConfig_DefaultQuietIsFalse(t *testing.T) { 97 | c := NewSwampConfig() 98 | c.targetRole = "some-role" 99 | c.targetAccount = "1234567890" 100 | 101 | assert.Equal(t, false, c.quiet) 102 | } 103 | 104 | func TestSwampConfig_Aliases(t *testing.T) { 105 | c := NewSwampConfig() 106 | c.aliasConfig = "does-not-exists" 107 | 108 | assert.Error(t, c.Validate()) 109 | } 110 | func TestSwampConfig_AliasesMissing(t *testing.T) { 111 | c := NewSwampConfig() 112 | c.aliasConfig = "CHANGELOG.md" 113 | 114 | assert.NoError(t, c.Validate()) 115 | } 116 | -------------------------------------------------------------------------------- /example/bash_aliases.sh: -------------------------------------------------------------------------------- 1 | # This aliases are generated with swamp 2 | 3 | function swamp-team1-nonlive-readonly() { 4 | SWAMP_TARGET_PROFILE='team1-nonlive-readonly' \ 5 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 6 | SWAMP_ACCOUNT_NAME='nonlive' \ 7 | SWAMP_TARGET_ROLE='readonly' \ 8 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'readonly' -target-profile 'team1-nonlive-readonly' "${@}" 9 | } 10 | 11 | function swamp-team1-nonlive-readonly-bash() { 12 | SWAMP_TARGET_PROFILE='team1-nonlive-readonly' \ 13 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 14 | SWAMP_ACCOUNT_NAME='nonlive' \ 15 | SWAMP_TARGET_ROLE='readonly' \ 16 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'readonly' -target-profile 'team1-nonlive-readonly' -exec "bash" 17 | } 18 | 19 | function swamp-team1-nonlive-readonly-info() { 20 | SWAMP_TARGET_PROFILE='team1-nonlive-readonly' \ 21 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 22 | SWAMP_ACCOUNT_NAME='nonlive' \ 23 | SWAMP_TARGET_ROLE='readonly' \ 24 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'readonly' -target-profile 'team1-nonlive-readonly' -exec "aws sts get-caller-identity --output json" 25 | } 26 | 27 | function swamp-team1-nonlive-readonly-tf-init() { 28 | SWAMP_TARGET_PROFILE='team1-nonlive-readonly' \ 29 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 30 | SWAMP_ACCOUNT_NAME='nonlive' \ 31 | SWAMP_TARGET_ROLE='readonly' \ 32 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'readonly' -target-profile 'team1-nonlive-readonly' -exec "cd '${1}' && terraform init" 33 | } 34 | 35 | function swamp-team1-nonlive-developer() { 36 | SWAMP_TARGET_PROFILE='team1-nonlive-developer' \ 37 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 38 | SWAMP_ACCOUNT_NAME='nonlive' \ 39 | SWAMP_TARGET_ROLE='developer' \ 40 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'developer' -target-profile 'team1-nonlive-developer' "${@}" 41 | } 42 | 43 | function swamp-team1-nonlive-developer-bash() { 44 | SWAMP_TARGET_PROFILE='team1-nonlive-developer' \ 45 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 46 | SWAMP_ACCOUNT_NAME='nonlive' \ 47 | SWAMP_TARGET_ROLE='developer' \ 48 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'developer' -target-profile 'team1-nonlive-developer' -exec "bash" 49 | } 50 | 51 | function swamp-team1-nonlive-developer-info() { 52 | SWAMP_TARGET_PROFILE='team1-nonlive-developer' \ 53 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 54 | SWAMP_ACCOUNT_NAME='nonlive' \ 55 | SWAMP_TARGET_ROLE='developer' \ 56 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'developer' -target-profile 'team1-nonlive-developer' -exec "aws sts get-caller-identity --output json" 57 | } 58 | 59 | function swamp-team1-nonlive-developer-tf-init() { 60 | SWAMP_TARGET_PROFILE='team1-nonlive-developer' \ 61 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 62 | SWAMP_ACCOUNT_NAME='nonlive' \ 63 | SWAMP_TARGET_ROLE='developer' \ 64 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'developer' -target-profile 'team1-nonlive-developer' -exec "cd '${1}' && terraform init" 65 | } 66 | 67 | function swamp-team1-nonlive-admin() { 68 | SWAMP_TARGET_PROFILE='team1-nonlive-admin' \ 69 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 70 | SWAMP_ACCOUNT_NAME='nonlive' \ 71 | SWAMP_TARGET_ROLE='admin' \ 72 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'admin' -target-profile 'team1-nonlive-admin' "${@}" 73 | } 74 | 75 | function swamp-team1-nonlive-admin-bash() { 76 | SWAMP_TARGET_PROFILE='team1-nonlive-admin' \ 77 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 78 | SWAMP_ACCOUNT_NAME='nonlive' \ 79 | SWAMP_TARGET_ROLE='admin' \ 80 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'admin' -target-profile 'team1-nonlive-admin' -exec "bash" 81 | } 82 | 83 | function swamp-team1-nonlive-admin-info() { 84 | SWAMP_TARGET_PROFILE='team1-nonlive-admin' \ 85 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 86 | SWAMP_ACCOUNT_NAME='nonlive' \ 87 | SWAMP_TARGET_ROLE='admin' \ 88 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'admin' -target-profile 'team1-nonlive-admin' -exec "aws sts get-caller-identity --output json" 89 | } 90 | 91 | function swamp-team1-nonlive-admin-tf-init() { 92 | SWAMP_TARGET_PROFILE='team1-nonlive-admin' \ 93 | SWAMP_ACCOUNT='XXXXXXXXX1' \ 94 | SWAMP_ACCOUNT_NAME='nonlive' \ 95 | SWAMP_TARGET_ROLE='admin' \ 96 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX1' -target-role 'admin' -target-profile 'team1-nonlive-admin' -exec "cd '${1}' && terraform init" 97 | } 98 | 99 | function swamp-team1-live-readonly() { 100 | SWAMP_TARGET_PROFILE='team1-live-readonly' \ 101 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 102 | SWAMP_ACCOUNT_NAME='live' \ 103 | SWAMP_TARGET_ROLE='readonly' \ 104 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'readonly' -target-profile 'team1-live-readonly' "${@}" 105 | } 106 | 107 | function swamp-team1-live-readonly-bash() { 108 | SWAMP_TARGET_PROFILE='team1-live-readonly' \ 109 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 110 | SWAMP_ACCOUNT_NAME='live' \ 111 | SWAMP_TARGET_ROLE='readonly' \ 112 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'readonly' -target-profile 'team1-live-readonly' -exec "bash" 113 | } 114 | 115 | function swamp-team1-live-readonly-info() { 116 | SWAMP_TARGET_PROFILE='team1-live-readonly' \ 117 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 118 | SWAMP_ACCOUNT_NAME='live' \ 119 | SWAMP_TARGET_ROLE='readonly' \ 120 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'readonly' -target-profile 'team1-live-readonly' -exec "aws sts get-caller-identity --output json" 121 | } 122 | 123 | function swamp-team1-live-readonly-tf-init() { 124 | SWAMP_TARGET_PROFILE='team1-live-readonly' \ 125 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 126 | SWAMP_ACCOUNT_NAME='live' \ 127 | SWAMP_TARGET_ROLE='readonly' \ 128 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'readonly' -target-profile 'team1-live-readonly' -exec "cd '${1}' && terraform init" 129 | } 130 | 131 | function swamp-team1-live-developer() { 132 | SWAMP_TARGET_PROFILE='team1-live-developer' \ 133 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 134 | SWAMP_ACCOUNT_NAME='live' \ 135 | SWAMP_TARGET_ROLE='developer' \ 136 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'developer' -target-profile 'team1-live-developer' "${@}" 137 | } 138 | 139 | function swamp-team1-live-developer-bash() { 140 | SWAMP_TARGET_PROFILE='team1-live-developer' \ 141 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 142 | SWAMP_ACCOUNT_NAME='live' \ 143 | SWAMP_TARGET_ROLE='developer' \ 144 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'developer' -target-profile 'team1-live-developer' -exec "bash" 145 | } 146 | 147 | function swamp-team1-live-developer-info() { 148 | SWAMP_TARGET_PROFILE='team1-live-developer' \ 149 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 150 | SWAMP_ACCOUNT_NAME='live' \ 151 | SWAMP_TARGET_ROLE='developer' \ 152 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'developer' -target-profile 'team1-live-developer' -exec "aws sts get-caller-identity --output json" 153 | } 154 | 155 | function swamp-team1-live-developer-tf-init() { 156 | SWAMP_TARGET_PROFILE='team1-live-developer' \ 157 | SWAMP_ACCOUNT='YYYYYYYYY1' \ 158 | SWAMP_ACCOUNT_NAME='live' \ 159 | SWAMP_TARGET_ROLE='developer' \ 160 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY1' -target-role 'developer' -target-profile 'team1-live-developer' -exec "cd '${1}' && terraform init" 161 | } 162 | 163 | function swamp-team1-infrastructure-readonly() { 164 | SWAMP_TARGET_PROFILE='team1-infrastructure-readonly' \ 165 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 166 | SWAMP_ACCOUNT_NAME='infrastructure' \ 167 | SWAMP_TARGET_ROLE='readonly' \ 168 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'readonly' -target-profile 'team1-infrastructure-readonly' "${@}" 169 | } 170 | 171 | function swamp-team1-infrastructure-readonly-bash() { 172 | SWAMP_TARGET_PROFILE='team1-infrastructure-readonly' \ 173 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 174 | SWAMP_ACCOUNT_NAME='infrastructure' \ 175 | SWAMP_TARGET_ROLE='readonly' \ 176 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'readonly' -target-profile 'team1-infrastructure-readonly' -exec "bash" 177 | } 178 | 179 | function swamp-team1-infrastructure-readonly-info() { 180 | SWAMP_TARGET_PROFILE='team1-infrastructure-readonly' \ 181 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 182 | SWAMP_ACCOUNT_NAME='infrastructure' \ 183 | SWAMP_TARGET_ROLE='readonly' \ 184 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'readonly' -target-profile 'team1-infrastructure-readonly' -exec "aws sts get-caller-identity --output json" 185 | } 186 | 187 | function swamp-team1-infrastructure-readonly-tf-init() { 188 | SWAMP_TARGET_PROFILE='team1-infrastructure-readonly' \ 189 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 190 | SWAMP_ACCOUNT_NAME='infrastructure' \ 191 | SWAMP_TARGET_ROLE='readonly' \ 192 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'readonly' -target-profile 'team1-infrastructure-readonly' -exec "cd '${1}' && terraform init" 193 | } 194 | 195 | function swamp-team1-infrastructure-developer() { 196 | SWAMP_TARGET_PROFILE='team1-infrastructure-developer' \ 197 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 198 | SWAMP_ACCOUNT_NAME='infrastructure' \ 199 | SWAMP_TARGET_ROLE='developer' \ 200 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'developer' -target-profile 'team1-infrastructure-developer' "${@}" 201 | } 202 | 203 | function swamp-team1-infrastructure-developer-bash() { 204 | SWAMP_TARGET_PROFILE='team1-infrastructure-developer' \ 205 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 206 | SWAMP_ACCOUNT_NAME='infrastructure' \ 207 | SWAMP_TARGET_ROLE='developer' \ 208 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'developer' -target-profile 'team1-infrastructure-developer' -exec "bash" 209 | } 210 | 211 | function swamp-team1-infrastructure-developer-info() { 212 | SWAMP_TARGET_PROFILE='team1-infrastructure-developer' \ 213 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 214 | SWAMP_ACCOUNT_NAME='infrastructure' \ 215 | SWAMP_TARGET_ROLE='developer' \ 216 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'developer' -target-profile 'team1-infrastructure-developer' -exec "aws sts get-caller-identity --output json" 217 | } 218 | 219 | function swamp-team1-infrastructure-developer-tf-init() { 220 | SWAMP_TARGET_PROFILE='team1-infrastructure-developer' \ 221 | SWAMP_ACCOUNT='ZZZZZZZZZ1' \ 222 | SWAMP_ACCOUNT_NAME='infrastructure' \ 223 | SWAMP_TARGET_ROLE='developer' \ 224 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ1' -target-role 'developer' -target-profile 'team1-infrastructure-developer' -exec "cd '${1}' && terraform init" 225 | } 226 | 227 | function swamp-team2-nonlive-admin() { 228 | SWAMP_TARGET_PROFILE='team2-nonlive-admin' \ 229 | SWAMP_ACCOUNT='XXXXXXXXX2' \ 230 | SWAMP_ACCOUNT_NAME='nonlive' \ 231 | SWAMP_TARGET_ROLE='admin' \ 232 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX2' -target-role 'admin' -target-profile 'team2-nonlive-admin' "${@}" 233 | } 234 | 235 | function swamp-team2-nonlive-admin-bash() { 236 | SWAMP_TARGET_PROFILE='team2-nonlive-admin' \ 237 | SWAMP_ACCOUNT='XXXXXXXXX2' \ 238 | SWAMP_ACCOUNT_NAME='nonlive' \ 239 | SWAMP_TARGET_ROLE='admin' \ 240 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX2' -target-role 'admin' -target-profile 'team2-nonlive-admin' -exec "bash" 241 | } 242 | 243 | function swamp-team2-nonlive-admin-info() { 244 | SWAMP_TARGET_PROFILE='team2-nonlive-admin' \ 245 | SWAMP_ACCOUNT='XXXXXXXXX2' \ 246 | SWAMP_ACCOUNT_NAME='nonlive' \ 247 | SWAMP_TARGET_ROLE='admin' \ 248 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX2' -target-role 'admin' -target-profile 'team2-nonlive-admin' -exec "aws sts get-caller-identity --output json" 249 | } 250 | 251 | function swamp-team2-nonlive-admin-tf-init() { 252 | SWAMP_TARGET_PROFILE='team2-nonlive-admin' \ 253 | SWAMP_ACCOUNT='XXXXXXXXX2' \ 254 | SWAMP_ACCOUNT_NAME='nonlive' \ 255 | SWAMP_TARGET_ROLE='admin' \ 256 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'XXXXXXXXX2' -target-role 'admin' -target-profile 'team2-nonlive-admin' -exec "cd '${1}' && terraform init" 257 | } 258 | 259 | function swamp-team2-live-readonly() { 260 | SWAMP_TARGET_PROFILE='team2-live-readonly' \ 261 | SWAMP_ACCOUNT='YYYYYYYYY2' \ 262 | SWAMP_ACCOUNT_NAME='live' \ 263 | SWAMP_TARGET_ROLE='readonly' \ 264 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY2' -target-role 'readonly' -target-profile 'team2-live-readonly' "${@}" 265 | } 266 | 267 | function swamp-team2-live-readonly-bash() { 268 | SWAMP_TARGET_PROFILE='team2-live-readonly' \ 269 | SWAMP_ACCOUNT='YYYYYYYYY2' \ 270 | SWAMP_ACCOUNT_NAME='live' \ 271 | SWAMP_TARGET_ROLE='readonly' \ 272 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY2' -target-role 'readonly' -target-profile 'team2-live-readonly' -exec "bash" 273 | } 274 | 275 | function swamp-team2-live-readonly-info() { 276 | SWAMP_TARGET_PROFILE='team2-live-readonly' \ 277 | SWAMP_ACCOUNT='YYYYYYYYY2' \ 278 | SWAMP_ACCOUNT_NAME='live' \ 279 | SWAMP_TARGET_ROLE='readonly' \ 280 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY2' -target-role 'readonly' -target-profile 'team2-live-readonly' -exec "aws sts get-caller-identity --output json" 281 | } 282 | 283 | function swamp-team2-live-readonly-tf-init() { 284 | SWAMP_TARGET_PROFILE='team2-live-readonly' \ 285 | SWAMP_ACCOUNT='YYYYYYYYY2' \ 286 | SWAMP_ACCOUNT_NAME='live' \ 287 | SWAMP_TARGET_ROLE='readonly' \ 288 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'YYYYYYYYY2' -target-role 'readonly' -target-profile 'team2-live-readonly' -exec "cd '${1}' && terraform init" 289 | } 290 | 291 | function swamp-team2-infrastructure-readonly() { 292 | SWAMP_TARGET_PROFILE='team2-infrastructure-readonly' \ 293 | SWAMP_ACCOUNT='ZZZZZZZZZ2' \ 294 | SWAMP_ACCOUNT_NAME='infrastructure' \ 295 | SWAMP_TARGET_ROLE='readonly' \ 296 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ2' -target-role 'readonly' -target-profile 'team2-infrastructure-readonly' "${@}" 297 | } 298 | 299 | function swamp-team2-infrastructure-readonly-bash() { 300 | SWAMP_TARGET_PROFILE='team2-infrastructure-readonly' \ 301 | SWAMP_ACCOUNT='ZZZZZZZZZ2' \ 302 | SWAMP_ACCOUNT_NAME='infrastructure' \ 303 | SWAMP_TARGET_ROLE='readonly' \ 304 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ2' -target-role 'readonly' -target-profile 'team2-infrastructure-readonly' -exec "bash" 305 | } 306 | 307 | function swamp-team2-infrastructure-readonly-info() { 308 | SWAMP_TARGET_PROFILE='team2-infrastructure-readonly' \ 309 | SWAMP_ACCOUNT='ZZZZZZZZZ2' \ 310 | SWAMP_ACCOUNT_NAME='infrastructure' \ 311 | SWAMP_TARGET_ROLE='readonly' \ 312 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ2' -target-role 'readonly' -target-profile 'team2-infrastructure-readonly' -exec "aws sts get-caller-identity --output json" 313 | } 314 | 315 | function swamp-team2-infrastructure-readonly-tf-init() { 316 | SWAMP_TARGET_PROFILE='team2-infrastructure-readonly' \ 317 | SWAMP_ACCOUNT='ZZZZZZZZZ2' \ 318 | SWAMP_ACCOUNT_NAME='infrastructure' \ 319 | SWAMP_TARGET_ROLE='readonly' \ 320 | swamp -region eu-central-1 -profile default -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/default' -account 'ZZZZZZZZZ2' -target-role 'readonly' -target-profile 'team2-infrastructure-readonly' -exec "cd '${1}' && terraform init" 321 | } 322 | 323 | function swamp-team3-nonlive-users-developer() { 324 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 325 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 326 | SWAMP_ACCOUNT_NAME='nonlive' \ 327 | SWAMP_TARGET_ROLE='users/Developer' \ 328 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' "${@}" 329 | } 330 | 331 | function swamp-team3-nonlive-users-developer-bash() { 332 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 333 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 334 | SWAMP_ACCOUNT_NAME='nonlive' \ 335 | SWAMP_TARGET_ROLE='users/Developer' \ 336 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "bash" 337 | } 338 | 339 | function swamp-team3-nonlive-users-developer-info() { 340 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 341 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 342 | SWAMP_ACCOUNT_NAME='nonlive' \ 343 | SWAMP_TARGET_ROLE='users/Developer' \ 344 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "aws sts get-caller-identity --output json" 345 | } 346 | 347 | function swamp-team3-nonlive-users-developer-tf-init() { 348 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 349 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 350 | SWAMP_ACCOUNT_NAME='nonlive' \ 351 | SWAMP_TARGET_ROLE='users/Developer' \ 352 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "cd '${1}' && terraform init" 353 | } 354 | 355 | function swamp-team3-nonlive-users-developer-build() { 356 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 357 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 358 | SWAMP_ACCOUNT_NAME='nonlive' \ 359 | SWAMP_TARGET_ROLE='users/Developer' \ 360 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "./ci/build.sh" 361 | } 362 | 363 | function swamp-team3-nonlive-users-developer-deploy() { 364 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 365 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 366 | SWAMP_ACCOUNT_NAME='nonlive' \ 367 | SWAMP_TARGET_ROLE='users/Developer' \ 368 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "./ci/deploy.sh \${SWAMP_ACCOUNT_NAME}" 369 | } 370 | 371 | function swamp-team3-nonlive-users-developer-tf-plan() { 372 | SWAMP_TARGET_PROFILE='team3-nonlive-users-developer' \ 373 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 374 | SWAMP_ACCOUNT_NAME='nonlive' \ 375 | SWAMP_TARGET_ROLE='users/Developer' \ 376 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Developer' -target-profile 'team3-nonlive-users-developer' -exec "cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan" 377 | } 378 | 379 | function swamp-team3-nonlive-users-admin() { 380 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 381 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 382 | SWAMP_ACCOUNT_NAME='nonlive' \ 383 | SWAMP_TARGET_ROLE='users/Admin' \ 384 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' "${@}" 385 | } 386 | 387 | function swamp-team3-nonlive-users-admin-bash() { 388 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 389 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 390 | SWAMP_ACCOUNT_NAME='nonlive' \ 391 | SWAMP_TARGET_ROLE='users/Admin' \ 392 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "bash" 393 | } 394 | 395 | function swamp-team3-nonlive-users-admin-info() { 396 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 397 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 398 | SWAMP_ACCOUNT_NAME='nonlive' \ 399 | SWAMP_TARGET_ROLE='users/Admin' \ 400 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "aws sts get-caller-identity --output json" 401 | } 402 | 403 | function swamp-team3-nonlive-users-admin-tf-init() { 404 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 405 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 406 | SWAMP_ACCOUNT_NAME='nonlive' \ 407 | SWAMP_TARGET_ROLE='users/Admin' \ 408 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "cd '${1}' && terraform init" 409 | } 410 | 411 | function swamp-team3-nonlive-users-admin-build() { 412 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 413 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 414 | SWAMP_ACCOUNT_NAME='nonlive' \ 415 | SWAMP_TARGET_ROLE='users/Admin' \ 416 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "./ci/build.sh" 417 | } 418 | 419 | function swamp-team3-nonlive-users-admin-deploy() { 420 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 421 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 422 | SWAMP_ACCOUNT_NAME='nonlive' \ 423 | SWAMP_TARGET_ROLE='users/Admin' \ 424 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "./ci/deploy.sh \${SWAMP_ACCOUNT_NAME}" 425 | } 426 | 427 | function swamp-team3-nonlive-users-admin-tf-plan() { 428 | SWAMP_TARGET_PROFILE='team3-nonlive-users-admin' \ 429 | SWAMP_ACCOUNT='XXXXXXXXXXX3' \ 430 | SWAMP_ACCOUNT_NAME='nonlive' \ 431 | SWAMP_TARGET_ROLE='users/Admin' \ 432 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'XXXXXXXXXXX3' -target-role 'users/Admin' -target-profile 'team3-nonlive-users-admin' -exec "cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan" 433 | } 434 | 435 | function swamp-team3-live-users-developer() { 436 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 437 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 438 | SWAMP_ACCOUNT_NAME='live' \ 439 | SWAMP_TARGET_ROLE='users/Developer' \ 440 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' "${@}" 441 | } 442 | 443 | function swamp-team3-live-users-developer-bash() { 444 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 445 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 446 | SWAMP_ACCOUNT_NAME='live' \ 447 | SWAMP_TARGET_ROLE='users/Developer' \ 448 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' -exec "bash" 449 | } 450 | 451 | function swamp-team3-live-users-developer-info() { 452 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 453 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 454 | SWAMP_ACCOUNT_NAME='live' \ 455 | SWAMP_TARGET_ROLE='users/Developer' \ 456 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' -exec "aws sts get-caller-identity --output json" 457 | } 458 | 459 | function swamp-team3-live-users-developer-tf-init() { 460 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 461 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 462 | SWAMP_ACCOUNT_NAME='live' \ 463 | SWAMP_TARGET_ROLE='users/Developer' \ 464 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' -exec "cd '${1}' && terraform init" 465 | } 466 | 467 | function swamp-team3-live-users-developer-deploy() { 468 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 469 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 470 | SWAMP_ACCOUNT_NAME='live' \ 471 | SWAMP_TARGET_ROLE='users/Developer' \ 472 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' -exec "./ci/deploy.sh \${SWAMP_ACCOUNT_NAME}" 473 | } 474 | 475 | function swamp-team3-live-users-developer-tf-plan() { 476 | SWAMP_TARGET_PROFILE='team3-live-users-developer' \ 477 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 478 | SWAMP_ACCOUNT_NAME='live' \ 479 | SWAMP_TARGET_ROLE='users/Developer' \ 480 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Developer' -target-profile 'team3-live-users-developer' -exec "cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan" 481 | } 482 | 483 | function swamp-team3-live-users-admin() { 484 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 485 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 486 | SWAMP_ACCOUNT_NAME='live' \ 487 | SWAMP_TARGET_ROLE='users/Admin' \ 488 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' "${@}" 489 | } 490 | 491 | function swamp-team3-live-users-admin-bash() { 492 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 493 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 494 | SWAMP_ACCOUNT_NAME='live' \ 495 | SWAMP_TARGET_ROLE='users/Admin' \ 496 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' -exec "bash" 497 | } 498 | 499 | function swamp-team3-live-users-admin-info() { 500 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 501 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 502 | SWAMP_ACCOUNT_NAME='live' \ 503 | SWAMP_TARGET_ROLE='users/Admin' \ 504 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' -exec "aws sts get-caller-identity --output json" 505 | } 506 | 507 | function swamp-team3-live-users-admin-tf-init() { 508 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 509 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 510 | SWAMP_ACCOUNT_NAME='live' \ 511 | SWAMP_TARGET_ROLE='users/Admin' \ 512 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' -exec "cd '${1}' && terraform init" 513 | } 514 | 515 | function swamp-team3-live-users-admin-deploy() { 516 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 517 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 518 | SWAMP_ACCOUNT_NAME='live' \ 519 | SWAMP_TARGET_ROLE='users/Admin' \ 520 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' -exec "./ci/deploy.sh \${SWAMP_ACCOUNT_NAME}" 521 | } 522 | 523 | function swamp-team3-live-users-admin-tf-plan() { 524 | SWAMP_TARGET_PROFILE='team3-live-users-admin' \ 525 | SWAMP_ACCOUNT='ZZZZZZZZZZZ3' \ 526 | SWAMP_ACCOUNT_NAME='live' \ 527 | SWAMP_TARGET_ROLE='users/Admin' \ 528 | swamp -region eu-central-1 -profile team3 -intermediate-profile team3-session -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB -mfa-exec 'pass otp aws.amazon.com/team3' -account 'ZZZZZZZZZZZ3' -target-role 'users/Admin' -target-profile 'team3-live-users-admin' -exec "cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan" 529 | } 530 | -------------------------------------------------------------------------------- /example/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | allArgs: >- 3 | -region eu-central-1 4 | allExecs: 5 | bash: bash 6 | info: aws sts get-caller-identity --output json 7 | tf-init: cd '${1}' && terraform init 8 | defaultAdditionalArgs: >- 9 | -profile default 10 | -mfa-device arn:aws:iam::AAAAAAAAA:mfa/BBBBBBBB 11 | -mfa-exec 'pass otp aws.amazon.com/default' 12 | teams: 13 | - name: team1 14 | accounts: 15 | - accountId: 'XXXXXXXXX1' 16 | name: nonlive 17 | roles: 18 | - readonly 19 | - developer 20 | - admin 21 | - accountId: 'YYYYYYYYY1' 22 | name: live 23 | roles: 24 | - readonly 25 | - developer 26 | - accountId: 'ZZZZZZZZZ1' 27 | name: infrastructure 28 | roles: 29 | - readonly 30 | - developer 31 | - name: team2 32 | accounts: 33 | - accountId: 'XXXXXXXXX2' 34 | name: nonlive 35 | roles: 36 | - admin 37 | - accountId: 'YYYYYYYYY2' 38 | name: live 39 | roles: 40 | - readonly 41 | - accountId: 'ZZZZZZZZZ2' 42 | name: infrastructure 43 | roles: 44 | - readonly 45 | - name: team3 46 | additionalArgs: >- 47 | -profile team3 48 | -intermediate-profile team3-session 49 | -mfa-device arn:aws:iam::CCCCCCCCCC:mfa/BBBBBBBB 50 | -mfa-exec 'pass otp aws.amazon.com/team3' 51 | accounts: 52 | - accountId: 'XXXXXXXXXXX3' 53 | name: nonlive 54 | roles: 55 | - users/Developer 56 | - users/Admin 57 | execs: 58 | build: ./ci/build.sh 59 | deploy: ./ci/deploy.sh \${SWAMP_ACCOUNT_NAME} 60 | tf-plan: cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan 61 | - accountId: 'ZZZZZZZZZZZ3' 62 | name: live 63 | roles: 64 | - users/Developer 65 | - users/Admin 66 | execs: 67 | deploy: ./ci/deploy.sh \${SWAMP_ACCOUNT_NAME} 68 | tf-plan: cd '${1}' && terraform workspace select \${SWAMP_ACCOUNT_NAME} && terraform plan 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/felixb/swamp 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.9.1 7 | github.com/aws/aws-sdk-go-v2/config v1.8.2 8 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.1 9 | github.com/go-ini/ini v1.63.2 10 | github.com/golang-interfaces/ios v0.0.0-20210510235934-31ebd5fb0b18 // indirect 11 | github.com/golang-utils/lockfile v0.0.0-20210511000054-2709c95799c7 12 | github.com/golang-utils/pscanary v0.0.0-20210511000120-691b61aef668 // indirect 13 | github.com/onsi/gomega v1.16.0 // indirect 14 | github.com/stretchr/testify v1.7.0 15 | gopkg.in/yaml.v2 v2.4.0 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.9.1 h1:ZbovGV/qo40nrOJ4q8G33AGICzaPI45FHQWJ9650pF4= 2 | github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= 3 | github.com/aws/aws-sdk-go-v2/config v1.8.2 h1:Dqy4ySXFmulRmZhfynm/5CD4Y6aXiTVhDtXLIuUe/r0= 4 | github.com/aws/aws-sdk-go-v2/config v1.8.2/go.mod h1:r0bkX9NyuCuf28qVcsEMtpAQibT7gA1Q0gzkjvgJdLU= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.4.2 h1:8kVE4Og6wlhVrMGiORQ3p9gRj2exjzhFRB+QzWBUa5Q= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.4.2/go.mod h1:9Sp6u121/f0NnvHyhG7dgoYeUTEFC2vsvJqJ6wXpkaI= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.1 h1:Nm+BxqBtT0r+AnD6byGMCGT4Km0QwHBy8mAYptNPXY4= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.1/go.mod h1:W1ldHfsgeGlKpJ4xZMKZUI6Wmp6EAstU7PxnhbXWWrI= 9 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.3 h1:NnXJXUz7oihrSlPKEM0yZ19b+7GQ47MX/LluLlEyE/Y= 10 | github.com/aws/aws-sdk-go-v2/internal/ini v1.2.3/go.mod h1:EES9ToeC3h063zCFDdqWGnARExNdULPaBvARm1FLwxA= 11 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.1 h1:APEjhKZLFlNVLATnA/TJyA+w1r/xd5r5ACWBDZ9aIvc= 12 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.1/go.mod h1:Ve+eJOx9UWaT/lMVebnFhDhO49fSLVedHoA82+Rqme0= 13 | github.com/aws/aws-sdk-go-v2/service/sso v1.4.1 h1:RfgQyv3bFT2Js6XokcrNtTjQ6wAVBRpoCgTFsypihHA= 14 | github.com/aws/aws-sdk-go-v2/service/sso v1.4.1/go.mod h1:ycPdbJZlM0BLhuBnd80WX9PucWPG88qps/2jl9HugXs= 15 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.1 h1:7ce9ugapSgBapwLhg7AJTqKW5U92VRX3vX65k2tsB+g= 16 | github.com/aws/aws-sdk-go-v2/service/sts v1.7.1/go.mod h1:r1i8QwKPzwByXqZb3POQfBs7jozrdnHz8PVbsvyx73w= 17 | github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc= 18 | github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 23 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 24 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 25 | github.com/go-ini/ini v1.63.2 h1:kwN3umicd2HF3Tgvap4um1ZG52/WyKT9GGdPx0CJk6Y= 26 | github.com/go-ini/ini v1.63.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 27 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 28 | github.com/golang-interfaces/ios v0.0.0-20210510235934-31ebd5fb0b18 h1:d7AL0bWA7Sg11f5qxzhr3goBTgPtqq8NQDMj09nGTAw= 29 | github.com/golang-interfaces/ios v0.0.0-20210510235934-31ebd5fb0b18/go.mod h1:aF6E7f2TOZM3ucm5Xcv8H9UJvKoPoWComRgDsktXIzM= 30 | github.com/golang-utils/lockfile v0.0.0-20210511000054-2709c95799c7 h1:1uc2TpJ15UhXZMXWQR3uPBp/eWC8IlYHYhk7OQjBapU= 31 | github.com/golang-utils/lockfile v0.0.0-20210511000054-2709c95799c7/go.mod h1:NTqD3Bo7jXafAd8czRuocL637kiUJ2HWvsvJK26CHJM= 32 | github.com/golang-utils/pscanary v0.0.0-20210511000120-691b61aef668 h1:dNbOHgQS05SDn5qdeYNwznQMXh9ldDQABNyiRcQisQ8= 33 | github.com/golang-utils/pscanary v0.0.0-20210511000120-691b61aef668/go.mod h1:IDUm8xWd84kcSlNSpeWUzvtT16pknpADFI7kX8YD+zg= 34 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 36 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 37 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 38 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 39 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 40 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 41 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 42 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 43 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 44 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 45 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 49 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 51 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 52 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 53 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 54 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 55 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 56 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 57 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 58 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 59 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 60 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 61 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 62 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 63 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 67 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 68 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 69 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 70 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 71 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 72 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 73 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 74 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 75 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 76 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 77 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 78 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 79 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 80 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 81 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 82 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= 96 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 98 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 101 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 104 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 105 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 106 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 107 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 108 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 109 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 111 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 112 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 113 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 114 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 115 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 116 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 117 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 119 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 121 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 122 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 123 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 125 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 126 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 127 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 128 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 129 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | // A Printer represents an active printer object that generates lines of 11 | // output to an io.Writer. Each output operation makes a single call to 12 | // the Writer's Write method. A Printer can be used simultaneously from 13 | // multiple goroutines; it guarantees to serialize access to the Writer. 14 | type Printer struct { 15 | mu sync.Mutex // ensures atomic writes; protects the following fields 16 | out io.Writer // destination for output 17 | buf []byte // for accumulating text to write 18 | off bool // should we be quiet? 19 | } 20 | 21 | // New creates a new Printer. The out variable sets the 22 | // destination to which log data will be written. 23 | func NewPrinter(out io.Writer) *Printer { 24 | return &Printer{out: out} 25 | } 26 | 27 | // Default printer. 28 | var printer = NewPrinter(os.Stdout) 29 | 30 | // SetOutput sets the output destination for the printer. 31 | func (p *Printer) SetOutput(w io.Writer) { 32 | p.mu.Lock() 33 | defer p.mu.Unlock() 34 | p.out = w 35 | } 36 | 37 | // Output writes to the output destination. A newline is appended 38 | // if the last character of s is not already a newline. 39 | func (p *Printer) Output(s string) error { 40 | if p.off { 41 | return nil 42 | } 43 | p.mu.Lock() 44 | defer p.mu.Unlock() 45 | p.buf = p.buf[:0] 46 | p.buf = append(p.buf, s...) 47 | if len(s) == 0 || s[len(s)-1] != '\n' { 48 | p.buf = append(p.buf, '\n') 49 | } 50 | _, err := p.out.Write(p.buf) 51 | return err 52 | } 53 | 54 | // Printf calls p.Output to print to the printer. 55 | // Arguments are handled in the manner of fmt.Printf. 56 | func (p *Printer) Printf(format string, v ...interface{}) { 57 | p.Output(fmt.Sprintf(format, v...)) 58 | } 59 | 60 | // Println calls p.Output to print to the printer. 61 | // Arguments are handled in the manner of fmt.Println. 62 | func (p *Printer) Println(v ...interface{}) { p.Output(fmt.Sprintln(v...)) } 63 | 64 | // Set quiet mode. 65 | func (p *Printer) SetOff(off bool) { 66 | p.mu.Lock() 67 | defer p.mu.Unlock() 68 | p.off = off 69 | } 70 | -------------------------------------------------------------------------------- /printer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // These tests are too simple. 4 | 5 | import ( 6 | "bytes" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | // Test using p.Println("hello", 23, "world") or using p.Printf("hello %d world", 23) 12 | func testPPrint(t *testing.T, useFormat bool, off bool) { 13 | buf := new(bytes.Buffer) 14 | p := NewPrinter(buf) 15 | p.SetOff(off) 16 | if useFormat { 17 | p.Printf("hello %d world", 23) 18 | } else { 19 | p.Println("hello", 23, "world") 20 | } 21 | line := buf.String() 22 | if len(line) > 0 { 23 | line = line[0 : len(line)-1] 24 | } 25 | if off { 26 | assert.Equal(t, "", buf.String()) 27 | } else { 28 | assert.Equal(t, "hello 23 world\n", buf.String()) 29 | } 30 | } 31 | 32 | func TestPAll(t *testing.T) { 33 | testPPrint(t, false, false) 34 | testPPrint(t, true, false) 35 | testPPrint(t, false, true) 36 | testPPrint(t, true, true) 37 | } 38 | -------------------------------------------------------------------------------- /profile_writer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 6 | "github.com/go-ini/ini" 7 | "github.com/golang-utils/lockfile" 8 | "os" 9 | "os/user" 10 | "path/filepath" 11 | "time" 12 | ) 13 | 14 | type ProfileWriter struct { 15 | lock lockfile.LockFile 16 | awsPath string 17 | credentialsPath string 18 | lockPath string 19 | } 20 | 21 | func NewProfileWriter() (*ProfileWriter, error) { 22 | if credentialsPath, err := getCredentialsPath(); err != nil { 23 | return nil, err 24 | } else { 25 | awsPath, _ := filepath.Split(credentialsPath) 26 | if awsPath == "" { 27 | return nil, fmt.Errorf("Error generating path for credentials file") 28 | } 29 | return &ProfileWriter{ 30 | lock: lockfile.New(), 31 | awsPath: awsPath, 32 | credentialsPath: credentialsPath, 33 | lockPath: filepath.Join(os.TempDir(), "swamp.lock"), 34 | }, nil 35 | } 36 | } 37 | 38 | func getCredentialsPath() (string, error) { 39 | credentialsPath := os.Getenv("AWS_SHARED_CREDENTIALS_FILE") 40 | if credentialsPath == "" { 41 | if usr, err := user.Current(); err != nil { 42 | return "", fmt.Errorf("Error fetching home dir: %s", err) 43 | } else { 44 | return filepath.Join(usr.HomeDir, ".aws", "credentials"), nil 45 | } 46 | } else { 47 | return credentialsPath, nil 48 | } 49 | } 50 | 51 | func (pw *ProfileWriter) WriteProfile(cred *types.Credentials, profileName, region string) error { 52 | pw.acquire_lock() 53 | defer pw.release_lock() 54 | 55 | if cfg, err := pw.getOrCreateCredentialsFile(); err != nil { 56 | return err 57 | } else { 58 | if sec, err := pw.getOrCreateSection(cfg, profileName); err != nil { 59 | return err 60 | } else { 61 | if err := pw.writeSection(sec, cred, region); err != nil { 62 | return err 63 | } 64 | 65 | if err := cfg.SaveTo(pw.credentialsPath); err != nil { 66 | return fmt.Errorf("Error writing credentials file: %s", err) 67 | } 68 | } 69 | } 70 | 71 | printer.Printf("Wrote session token for profile %s\n", profileName) 72 | printer.Printf("Token is valid until: %v\n", cred.Expiration) 73 | 74 | return nil 75 | } 76 | 77 | func (pw *ProfileWriter) acquire_lock() { 78 | for { 79 | if err := pw.lock.Lock(pw.lockPath); err == nil { 80 | return 81 | } else { 82 | fmt.Printf("Waiting for lock %s\n", pw.lockPath) 83 | time.Sleep(time.Second) 84 | } 85 | } 86 | } 87 | 88 | func (pw *ProfileWriter) release_lock() { 89 | os.Remove(pw.lockPath) 90 | } 91 | 92 | func (pw *ProfileWriter) getOrCreateCredentialsFile() (*ini.File, error) { 93 | if _, err := os.Stat(pw.awsPath); err != nil { 94 | if err := os.MkdirAll(pw.awsPath, os.ModePerm); err != nil { 95 | return nil, fmt.Errorf("Error creating aws config path %s: %s", pw.awsPath, err) 96 | } 97 | } 98 | 99 | cfg, err := ini.Load(pw.credentialsPath) 100 | if err != nil { 101 | fmt.Printf("Unable to find credentials file %s. Creating new file.\n", pw.credentialsPath) 102 | cfg = ini.Empty() 103 | } 104 | return cfg, nil 105 | } 106 | 107 | func (pw *ProfileWriter) getOrCreateSection(cfg *ini.File, profileName string) (*ini.Section, error) { 108 | sec, err := cfg.GetSection(profileName) 109 | if err != nil { 110 | if sec, err = cfg.NewSection(profileName); err != nil { 111 | return nil, fmt.Errorf("Error creating new profile %s: %s", profileName, err) 112 | } 113 | } 114 | return sec, err 115 | } 116 | 117 | func (pw *ProfileWriter) writeSection(sec *ini.Section, cred *types.Credentials, region string) error { 118 | if err := pw.writeKey(sec, "aws_access_key_id", *cred.AccessKeyId); err != nil { 119 | return err 120 | } 121 | if err := pw.writeKey(sec, "aws_secret_access_key", *cred.SecretAccessKey); err != nil { 122 | return err 123 | } 124 | if err := pw.writeKey(sec, "aws_session_token", *cred.SessionToken); err != nil { 125 | return err 126 | } 127 | if region != "" { 128 | if err := pw.writeKey(sec, "region", region); err != nil { 129 | return err 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | func (pw *ProfileWriter) writeKey(sec *ini.Section, name string, value string) error { 136 | if key, err := sec.GetKey(name); err != nil { 137 | if _, err := sec.NewKey(name, value); err != nil { 138 | return fmt.Errorf("Error writing config key %s: %s", name, err) 139 | } 140 | } else { 141 | key.SetValue(value) 142 | } 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /profile_writer_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go-v2/aws" 6 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 7 | "github.com/stretchr/testify/assert" 8 | "io/ioutil" 9 | "os" 10 | "os/user" 11 | "path" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func TestProfileWriter_NewProfileWriterWithDefaults(t *testing.T) { 17 | pw, err := NewProfileWriter() 18 | assert.NoError(t, err) 19 | assert.NotNil(t, pw) 20 | 21 | usr, _ := user.Current() 22 | awsPath := filepath.Join(usr.HomeDir, ".aws") 23 | credPath := filepath.Join(awsPath, "credentials") 24 | 25 | assert.Equal(t, awsPath+"/", pw.awsPath) 26 | assert.Equal(t, credPath, pw.credentialsPath) 27 | } 28 | 29 | func TestProfileWriter_NewProfileWriterWithEnvironment(t *testing.T) { 30 | awsPath := "/tmp/foo/" 31 | credPath := "/tmp/foo/.aws-secrets" 32 | os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credPath) 33 | defer os.Clearenv() 34 | 35 | pw, err := NewProfileWriter() 36 | assert.NoError(t, err) 37 | assert.NotNil(t, pw) 38 | 39 | assert.Equal(t, awsPath, pw.awsPath) 40 | assert.Equal(t, credPath, pw.credentialsPath) 41 | } 42 | 43 | func TestProfileWriter_WriteProfile(t *testing.T) { 44 | credPath := path.Join(os.TempDir(), "swamp-test.ini") 45 | os.Remove(credPath) 46 | 47 | os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credPath) 48 | defer os.Clearenv() 49 | defer os.Remove(credPath) 50 | 51 | profileName := "some-profile" 52 | region := "some-region" 53 | creds := &types.Credentials{ 54 | AccessKeyId: aws.String("some-access-key"), 55 | SecretAccessKey: aws.String("some-secret-access-key"), 56 | SessionToken: aws.String("some-session-token"), 57 | } 58 | 59 | pw, _ := NewProfileWriter() 60 | pw.WriteProfile(creds, profileName, region) 61 | 62 | b, err := ioutil.ReadFile(credPath) 63 | assert.NoError(t, err) 64 | 65 | content := string(b) 66 | 67 | assert.Regexp(t, `^\[some-profile\]\n.*`, content) 68 | assertKeyValue(t, "region", "some-region", content) 69 | assertKeyValue(t, "aws_access_key_id", "some-access-key", content) 70 | assertKeyValue(t, "aws_secret_access_key", "some-secret-access-key", content) 71 | assertKeyValue(t, "aws_session_token", "some-session-token", content) 72 | } 73 | 74 | func TestProfileWriter_WriteProfileWoRegion(t *testing.T) { 75 | credPath := path.Join(os.TempDir(), "swamp-test.ini") 76 | os.Remove(credPath) 77 | 78 | os.Setenv("AWS_SHARED_CREDENTIALS_FILE", credPath) 79 | defer os.Clearenv() 80 | defer os.Remove(credPath) 81 | 82 | profileName := "some-profile" 83 | region := "" 84 | creds := &types.Credentials{ 85 | AccessKeyId: aws.String("some-access-key"), 86 | SecretAccessKey: aws.String("some-secret-access-key"), 87 | SessionToken: aws.String("some-session-token"), 88 | } 89 | 90 | pw, _ := NewProfileWriter() 91 | pw.WriteProfile(creds, profileName, region) 92 | 93 | b, err := ioutil.ReadFile(credPath) 94 | assert.NoError(t, err) 95 | 96 | content := string(b) 97 | 98 | assert.Regexp(t, `^\[some-profile\]\n.*`, content) 99 | assert.NotRegexp(t, `region`, content) 100 | assertKeyValue(t, "aws_access_key_id", "some-access-key", content) 101 | assertKeyValue(t, "aws_secret_access_key", "some-secret-access-key", content) 102 | assertKeyValue(t, "aws_session_token", "some-session-token", content) 103 | } 104 | 105 | func assertKeyValue(t *testing.T, key, value, content string) { 106 | assert.Regexp(t, fmt.Sprintf(`\n%s\s*=\s*%s\n.*`, key, value), content) 107 | } 108 | -------------------------------------------------------------------------------- /swamp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/aws-sdk-go-v2/service/sts" 11 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 12 | "os" 13 | "os/exec" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func die(msg string, err error) { 19 | dieSlow(msg, "", err) 20 | } 21 | 22 | func dieSlow(msg, longMsg string, err error) { 23 | fmt.Fprintln(os.Stderr, msg+":") 24 | fmt.Fprintln(os.Stderr, "") 25 | fmt.Fprintln(os.Stderr, err) 26 | if longMsg != "" { 27 | fmt.Fprintln(os.Stderr, "") 28 | fmt.Fprintln(os.Stderr, longMsg) 29 | } 30 | os.Exit(1) 31 | } 32 | 33 | func getCallerId(ctx context.Context, svc *sts.Client) *sts.GetCallerIdentityOutput { 34 | output, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) 35 | if err != nil { 36 | die("Error fetching caller id", err) 37 | } 38 | 39 | return output 40 | } 41 | 42 | func cleanTokenCode(tokenCode string) string { 43 | return strings.Trim(tokenCode, " \r\n") 44 | } 45 | 46 | func fetchTokenCode(tokenSerialNumber string, cmd string) string { 47 | printer.Printf("Obtaining mfa token for: %s\n", tokenSerialNumber) 48 | if output, err := exec.Command("/bin/sh", "-c", cmd).Output(); err != nil { 49 | die("Error obtaining mfa token", err) 50 | return "" 51 | } else { 52 | return string(output) 53 | } 54 | } 55 | 56 | func askForTokenCode(tokenSerialNumber string) string { 57 | reader := bufio.NewReader(os.Stdin) 58 | fmt.Printf("Enter mfa token for %s: ", tokenSerialNumber) 59 | if tokenCode, err := reader.ReadString('\n'); err != nil { 60 | die("Error reading mfa token", err) 61 | return "" 62 | } else { 63 | return tokenCode 64 | } 65 | } 66 | 67 | func getTokenCode(swampConfig *SwampConfig) string { 68 | var tokenCode string 69 | if swampConfig.mfaExec != "" { 70 | tokenCode = fetchTokenCode(swampConfig.tokenSerialNumber, swampConfig.mfaExec) 71 | } else { 72 | tokenCode = askForTokenCode(swampConfig.tokenSerialNumber) 73 | } 74 | return cleanTokenCode(tokenCode) 75 | } 76 | 77 | func validateSessionToken(ctx context.Context, awsConfig aws.Config) bool { 78 | svc := sts.NewFromConfig(awsConfig) 79 | _, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) 80 | return err == nil 81 | } 82 | 83 | func guessCurrentProfile(swampConfig *SwampConfig) string { 84 | if swampConfig.profile != "" { 85 | return swampConfig.profile 86 | } 87 | 88 | profileFromEnv := os.Getenv("AWS_PROFILE") 89 | if profileFromEnv != "" { 90 | return profileFromEnv 91 | } 92 | 93 | return "default" 94 | } 95 | 96 | func getSessionToken(ctx context.Context, swampConfig *SwampConfig, awsConfig aws.Config) *types.Credentials { 97 | svc := sts.NewFromConfig(awsConfig) 98 | tokenCode := getTokenCode(swampConfig) 99 | output, err := svc.GetSessionToken(ctx, &sts.GetSessionTokenInput{ 100 | DurationSeconds: aws.Int32(int32(swampConfig.intermediateDuration)), 101 | SerialNumber: &swampConfig.tokenSerialNumber, 102 | TokenCode: &tokenCode, 103 | }) 104 | if err != nil { 105 | dieSlow("Error getting session token", fmt.Sprintf(`Make sure your current profile %s is valid and allows running "aws sts get-session-token".`, guessCurrentProfile(swampConfig)), err) 106 | } 107 | 108 | return output.Credentials 109 | } 110 | 111 | func getIntermediateSessionOptions(ctx context.Context, swampConfig *SwampConfig) aws.Config { 112 | return newSessionOptions(ctx, swampConfig.intermediateProfile, swampConfig.region) 113 | } 114 | 115 | func getBaseSessionOptions(ctx context.Context, swampConfig *SwampConfig) aws.Config { 116 | return newSessionOptions(ctx, swampConfig.profile, swampConfig.region) 117 | } 118 | 119 | func newSessionOptions(ctx context.Context, profile, region string) aws.Config { 120 | if cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region), config.WithSharedConfigProfile(profile)); err != nil { 121 | die("Error loading aws config", err) 122 | return cfg 123 | } else { 124 | return cfg 125 | } 126 | } 127 | 128 | // validate session token and request a new one if it's invalid. 129 | // write target profile into .aws/credentials 130 | func ensureSessionTokenProfile(ctx context.Context, swampConfig *SwampConfig, pw *ProfileWriter) { 131 | printer.Printf("Checking if profile %s is still valid\n", swampConfig.intermediateProfile) 132 | if validateSessionToken(ctx, getIntermediateSessionOptions(ctx, swampConfig)) { 133 | printer.Printf("Session token for profile %s is still valid\n", swampConfig.intermediateProfile) 134 | } else { 135 | awsConfig := getBaseSessionOptions(ctx, swampConfig) 136 | cred := getSessionToken(ctx, swampConfig, awsConfig) 137 | if err := pw.WriteProfile(cred, swampConfig.intermediateProfile, awsConfig.Region); err != nil { 138 | die("Error writing profile", err) 139 | } 140 | } 141 | } 142 | 143 | func assumeRole(ctx context.Context, svc *sts.Client, roleArn, roleSessionName *string, duration *int32) *types.Credentials { 144 | output, err := svc.AssumeRole(ctx, &sts.AssumeRoleInput{ 145 | RoleArn: roleArn, 146 | RoleSessionName: roleSessionName, 147 | DurationSeconds: duration, 148 | }) 149 | if err != nil { 150 | dieSlow("Error assuming role", fmt.Sprintf(`Make sure your current profile is valid and allows running "aws sts assume-role --role-arn %s"`, *roleArn), err) 151 | } 152 | 153 | return output.Credentials 154 | } 155 | 156 | // assume-role into target account and write target profile into .aws/credentials 157 | func ensureTargetProfile(ctx context.Context, swampConfig *SwampConfig, pw *ProfileWriter, awsConfig aws.Config) { 158 | svc := sts.NewFromConfig(awsConfig) 159 | 160 | userId := getCallerId(ctx, svc).Arn 161 | parts := strings.Split(*userId, "/") 162 | roleSessionName := parts[len(parts)-1] 163 | 164 | cred := assumeRole(ctx, svc, swampConfig.GetRoleArn(), &roleSessionName, aws.Int32(int32(swampConfig.targetDuration))) 165 | if err := pw.WriteProfile(cred, swampConfig.targetProfile, awsConfig.Region); err != nil { 166 | die("Error writing profile", err) 167 | } 168 | } 169 | 170 | func cleanCredentialsFromEnv(env []string) []string { 171 | ret := env 172 | 173 | for _, key := range [...]string{"AWS_ACCESS_KEY_ID=", "AWS_SECRET_ACCESS_KEY=", "AWS_SESSION_TOKEN="} { 174 | for i, e := range ret { 175 | if strings.HasPrefix(e, key) { 176 | ret = append(ret[:i], ret[i+1:]...) 177 | break 178 | } 179 | } 180 | } 181 | 182 | return ret 183 | } 184 | 185 | func execCommand(swampConfig *SwampConfig) error { 186 | c := exec.Command("/bin/sh", "-c", swampConfig.exec) 187 | c.Env = append(cleanCredentialsFromEnv(os.Environ()), fmt.Sprintf("AWS_PROFILE=%s", swampConfig.targetProfile)) 188 | c.Stdin = os.Stdin 189 | c.Stdout = os.Stdout 190 | c.Stderr = os.Stderr 191 | return c.Run() 192 | } 193 | 194 | func main() { 195 | // set up command line flags 196 | swampConfig := NewSwampConfig() 197 | swampConfig.SetupFlags() 198 | flag.Parse() 199 | 200 | // setup logging 201 | if swampConfig.quiet { 202 | printer.SetOff(true) 203 | } 204 | 205 | // check user input on command line flags 206 | if err := swampConfig.Validate(); err != nil { 207 | fmt.Fprintln(os.Stderr, err) 208 | flag.Usage() 209 | os.Exit(1) 210 | } 211 | if swampConfig.aliasConfig == "" { 212 | assume(context.Background(), swampConfig) 213 | } else { 214 | if err := generateAliases(os.Stdout, swampConfig.aliasConfig); err != nil { 215 | die("Error generating alias swampConfig", err) 216 | } 217 | } 218 | } 219 | 220 | func assume(ctx context.Context, swampConfig *SwampConfig) { 221 | baseProfile := swampConfig.profile 222 | if swampConfig.tokenSerialNumber != "" { 223 | baseProfile = swampConfig.intermediateProfile 224 | } 225 | pw, err := NewProfileWriter() 226 | if err != nil { 227 | die("Error initializing profile writer", err) 228 | } 229 | for { 230 | if swampConfig.tokenSerialNumber != "" { 231 | // get intermediate session token with mfa, use that to assume role into target account 232 | ensureSessionTokenProfile(ctx, swampConfig, pw) 233 | } 234 | 235 | if swampConfig.targetRole != "" { 236 | ensureTargetProfile(ctx, swampConfig, pw, newSessionOptions(ctx, baseProfile, swampConfig.region)) 237 | 238 | if swampConfig.exec != "" { 239 | if err := execCommand(swampConfig); err != nil { 240 | die(fmt.Sprintf(`Error running command: "%s" with AWS profile: "%s"`, swampConfig.exec, swampConfig.targetProfile), err) 241 | } else { 242 | printer.Printf("Executed \"%s\" successfully\n", swampConfig.exec) 243 | } 244 | } 245 | } 246 | 247 | if !swampConfig.renew { 248 | break 249 | } 250 | time.Sleep(time.Second * time.Duration(swampConfig.targetDuration/2)) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /swamp_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestSwamp_ExecutingMFACommand(t *testing.T) { 10 | tokenCode := fetchTokenCode("some-device-id", "echo 1234") 11 | 12 | assert.EqualValues(t, "1234\n", tokenCode) 13 | } 14 | 15 | func TestSwamp_GetTokenCodeWithMFACommandTrimNewLine(t *testing.T) { 16 | config := NewSwampConfig() 17 | config.tokenSerialNumber = "some-device-id" 18 | config.mfaExec = "echo 123456\n" 19 | 20 | tokenCode := getTokenCode(config) 21 | 22 | assert.EqualValues(t, "123456", tokenCode) 23 | } 24 | 25 | func TestSwamp_ExecCommand_ExitCode_Zero(t *testing.T) { 26 | config := NewSwampConfig() 27 | config.exec = "true" 28 | 29 | err := execCommand(config) 30 | 31 | assert.NoError(t, err) 32 | } 33 | 34 | func TestSwamp_ExecCommand_ExitCode_NonZero(t *testing.T) { 35 | config := NewSwampConfig() 36 | config.exec = "false" 37 | 38 | err := execCommand(config) 39 | 40 | assert.Error(t, err) 41 | } 42 | 43 | func TestSwamp_ExecCommand_ExitCode_EnvironmentHasNotAwsCredentials(t *testing.T) { 44 | os.Setenv("AWS_ACCESS_KEY_ID", "some-access-key-id") 45 | os.Setenv("AWS_SECRET_ACCESS_KEY", "some-secret-access-key") 46 | os.Setenv("AWS_SESSION_TOKEN", "some-session-token") 47 | 48 | config := NewSwampConfig() 49 | config.targetProfile = "some-target-profile" 50 | config.exec = `test -z "${AWS_ACCESS_KEY_ID}" && test -z "${AWS_SECRET_ACCESS_KEY}" && test -z "${AWS_SESSION_TOKEN}"` 51 | 52 | err := execCommand(config) 53 | 54 | assert.NoError(t, err) 55 | } 56 | --------------------------------------------------------------------------------