├── aws-whoami ├── .gitignore ├── main_test.go └── main.go ├── .github └── workflows │ └── release.yaml ├── go.mod ├── CHANGELOG.md ├── go.sum ├── README.md └── LICENSE /aws-whoami/.gitignore: -------------------------------------------------------------------------------- 1 | aws-whoami 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: created 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | # build and publish in parallel: linux/amd64, linux/arm64, windows/amd64, darwin/amd64, darwin/arm64 12 | goos: [linux, windows, darwin] 13 | goarch: [amd64, arm64] 14 | exclude: 15 | - goarch: arm64 16 | goos: windows 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: wangyoucao577/go-release-action@v1.35 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | goos: ${{ matrix.goos }} 23 | goarch: ${{ matrix.goarch }} 24 | extra_files: LICENSE README.md 25 | compress_assets: "true" 26 | md5sum: "false" 27 | project_path: aws-whoami 28 | binary_name: aws-whoami 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/benkehoe/aws-whoami-golang/v2 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.17.5 7 | github.com/aws/aws-sdk-go-v2/config v1.18.14 8 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.3 9 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.4 10 | github.com/aws/smithy-go v1.13.5 11 | ) 12 | 13 | require ( 14 | github.com/aws/aws-sdk-go-v2/credentials v1.13.14 // indirect 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.3 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | `aws-whoami` uses [monotonic versioning](https://github.com/benkehoe/monotonic-versioning-manifesto) since v1.0 across both [the older Python implementation](https://github.com/benkehoe/aws-whoami) (compatibility number 1) and this Go implementation (compatibility number 2). 4 | 5 | ## v2.6 6 | 7 | * With `--json`, errors are printed as JSON in the form `{"Error": "The error message"}` 8 | 9 | ## v2.5 10 | 11 | * Add `--disable-account-alias` flag. 12 | * Handle paths in user ARNs. 13 | * Disable account alias check by matching SSO Permission Set name. 14 | * Internal revamp for testing. 15 | * Change repo layout to work better with `go install`. 16 | * Add tests. 17 | 18 | ## v2.4 19 | 20 | * Handle root user 21 | 22 | ## v2.3 23 | 24 | * Initial implementation in Go. 25 | * `--debug` has been removed. 26 | * A region is now required (this appears to be an SDK constraint). 27 | 28 | ## v1.2 29 | 30 | Latest version of [the Python implementation](https://github.com/benkehoe/aws-whoami). 31 | 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= 2 | github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 3 | github.com/aws/aws-sdk-go-v2/config v1.18.14 h1:rI47jCe0EzuJlAO5ptREe3LIBAyP5c7gR3wjyYVjuOM= 4 | github.com/aws/aws-sdk-go-v2/config v1.18.14/go.mod h1:0pI6JQBHKwd0JnwAZS3VCapLKMO++UL2BOkWwyyzTnA= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.13.14 h1:jE34fUepssrhmYpvPpdbd+d39PHpuignDpNPNJguP60= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.13.14/go.mod h1:85ckagDuzdIOnZRwws1eLKnymJs3ZM1QwVC1XcuNGOY= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM= 15 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.3 h1:vaDXu/p/5RNK++B2pqKS3hwsWGxsVjJJqPaAMg0OkiM= 16 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.3/go.mod h1:F5Xt96+AfAiyMpRXHy9CKafE/KULVwj7MwgZ0a4row4= 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls= 18 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44= 19 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.3 h1:bUeZTWfF1vBdZnoNnnq70rB/CzdZD7NR2Jg2Ax+rvjA= 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.3/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw= 21 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3 h1:G/+7NUi+q+H0LG3v32jfV4OkaQIcpI92g0owbXKk6NY= 22 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.3/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc= 23 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.4 h1:j0USUNbl9c/8tBJ8setEbwxc7wva0WyoeAaFRiyTUT8= 24 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.4/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s= 25 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 26 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 27 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 29 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 31 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-whoami 2 | **Find what AWS account and identity you're using** 3 | 4 | > :warning: This is the successor to [the python implementation](https://github.com/benkehoe/aws-whoami) as a CLI tool. The other is still useful as a Python library. 5 | 6 | You should know about [`aws sts get-caller-identity`](https://docs.aws.amazon.com/cli/latest/reference/sts/get-caller-identity.html), which sensibly returns the identity of the caller. 7 | But even with `--output table`, I find this a bit lacking. 8 | That ARN is a lot to visually parse, it doesn't tell you what region your credentials are configured for, and I am not very good at remembering AWS account numbers. `aws-whoami` makes it better. 9 | 10 | ``` 11 | $ aws-whoami 12 | Account: 123456789012 13 | my-account-alias 14 | Region: us-east-2 15 | AssumedRole: MyRole 16 | RoleSessionName: ben 17 | UserId: AROASOMEOPAQUEID:ben 18 | Arn: arn:aws:sts::123456789012:assumed-role/MyRole/ben 19 | ``` 20 | 21 | Note: if you don't have permissions to [iam:ListAccountAliases](https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListAccountAliases.html), your account alias won't appear. 22 | See below for disabling this check if getting a permission denied on this call raises flags in your organization. 23 | 24 | ## Install 25 | 26 | ``` 27 | go install github.com/benkehoe/aws-whoami-golang/v2/aws-whoami@latest 28 | ``` 29 | 30 | [Or download the latest release for your platform](https://github.com/benkehoe/aws-whoami-golang/releases/latest). 31 | 32 | 33 | ## Options 34 | 35 | `aws-whoami` uses [`the AWS Go SDK v2`](https://aws.amazon.com/sdk-for-go/), so it'll pick up your credentials in [the normal ways](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#config-settings-and-precedence), including with the `--profile` parameter. 36 | 37 | If you'd like the output as a JSON object, use the `--json` flag. 38 | See below for field names. 39 | 40 | The `--disable-account-alias` flag disables account alias checking (see below). 41 | 42 | Use `--version` to output the version. 43 | 44 | ## Account alias checking 45 | 46 | By default, `aws-whoami` calls the [IAM `ListAccountAliases` API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListAccountAliases.html) to find the account name, if set. 47 | If you don't have access to this API (the `iam:ListAccountAliases` IAM action), it swallows that error. 48 | In general this is fine, but if it causes trouble (e.g., raising security alerts in your organization), you can disable it. 49 | 50 | There are two ways to disable account alias checking. 51 | The first is the `--disable-account-alias` flag. 52 | The second, setting the environment variable `AWS_WHOAMI_DISABLE_ACCOUNT_ALIAS`, allows for persistent and selective control. 53 | 54 | To fully disable account alias checking, set `AWS_WHOAMI_DISABLE_ACCOUNT_ALIAS` to `true`. 55 | To selectively disable it, you can also set the value to a comma-separated list where each item will be matched against the following: 56 | * The beginning or end of the account number 57 | * The principal name or ARN 58 | * The role session name 59 | * The SSO role (permission set) name 60 | 61 | ## JSON output 62 | 63 | The JSON object that is printed when using the `--json` flag always (when successful, see below for errors) includes the following fields: 64 | * `Account` 65 | * `AccountAliases` (NOTE: this is a list) 66 | * `Arn` 67 | * `Type` 68 | * `Name` 69 | * `RoleSessionName` 70 | * `UserId` 71 | * `Region` 72 | * `SSOPermissionSet` 73 | 74 | `Type`, `Name`, and `RoleSessionName` (and `SSOPermissionSet`) are split from the ARN for convenience. 75 | `RoleSessionName` is `null` for IAM users. 76 | For the account root, both the `Type` and `Name` are `"root"`. 77 | 78 | `SSOPermissionSet` is set if the assumed role name conforms to the format `AWSReservedSSO_{permission-set}_{random-tag}`, otherwise it is `null`. 79 | 80 | Note that the `AccountAliases` field is an empty list when account alias checking is disabled, not `null`. 81 | 82 | If there is an error, a JSON object is printed with the following structure: `{"Error": "The error message"}` 83 | 84 | ## AWS CLI alias 85 | 86 | The AWS CLI has a way to add [command aliases](https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-alias.html), and you can use this with `aws-whoami`. 87 | In `~/.aws/cli/alias`, add `whoami = !aws-whoami` under the `[toplevel]` section, like this: 88 | 89 | ```ini 90 | [toplevel] 91 | 92 | whoami = !aws-whoami 93 | ``` 94 | 95 | Now you can run the command `aws whoami` as if it was part of the AWS CLI. 96 | 97 | Of course, even if you're not using `aws-whoami` you can create `aws whoami` as an alias for `GetCallerIdentity` directly, like this: 98 | 99 | ```ini 100 | [toplevel] 101 | 102 | whoami = sts get-caller-identity --output table 103 | ``` 104 | -------------------------------------------------------------------------------- /aws-whoami/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/sts" 7 | ) 8 | 9 | func NewGetCallerIdentityOutput(account string, arn string, userId string) sts.GetCallerIdentityOutput { 10 | var getCallerIdentityOutput sts.GetCallerIdentityOutput 11 | getCallerIdentityOutput.Account = &account 12 | getCallerIdentityOutput.Arn = &arn 13 | getCallerIdentityOutput.UserId = &userId 14 | return getCallerIdentityOutput 15 | } 16 | 17 | func newWhoami(account string, arn string, userId string, accountAliases []string, region string) Whoami { 18 | whoami := Whoami{} 19 | whoami.AccountAliases = accountAliases 20 | whoami.Region = region 21 | 22 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 23 | 24 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 25 | 26 | return whoami 27 | } 28 | 29 | func TestBasic(t *testing.T) { 30 | account := "123456789012" 31 | arn := "arn:aws:iam::123456789012:assumed-role/ben/my-session" 32 | userId := "AIDAJQABLZS4A3QDU576Q" 33 | 34 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 35 | 36 | whoami := Whoami{} 37 | 38 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 39 | 40 | if whoami.Account != account { 41 | t.Fatalf("Account set incorrectly to '%v'", whoami.Account) 42 | } 43 | 44 | if whoami.Arn != arn { 45 | t.Fatalf("Arn set incorrectly to '%v'", whoami.Arn) 46 | } 47 | 48 | if whoami.UserId != userId { 49 | t.Fatalf("UserId set incorrectly to '%v'", whoami.UserId) 50 | } 51 | } 52 | 53 | func TestRootArn(t *testing.T) { 54 | account := "123456789012" 55 | arn := "arn:aws:iam::123456789012:root" 56 | userId := "AIDAJQABLZS4A3QDU576Q" 57 | 58 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 59 | 60 | whoami := Whoami{} 61 | 62 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 63 | 64 | if whoami.Type != "root" { 65 | t.Fatalf("Type is %v (should be root)", whoami.Type) 66 | } 67 | 68 | if whoami.Name != "root" { 69 | t.Fatalf("Name is %v (should be root)", whoami.Name) 70 | } 71 | 72 | if whoami.RoleSessionName != nil { 73 | t.Fatalf("RoleSessionName is set (to %v)", *whoami.RoleSessionName) 74 | } 75 | 76 | if whoami.SSOPermissionSet != nil { 77 | t.Fatalf("SSOPermissionSet is set (to %v)", *whoami.SSOPermissionSet) 78 | } 79 | } 80 | 81 | func TestUserArn(t *testing.T) { 82 | account := "123456789012" 83 | arn := "arn:aws:iam::123456789012:user/some/path/ben" 84 | userId := "AIDAJQABLZS4A3QDU576Q" 85 | 86 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 87 | 88 | whoami := Whoami{} 89 | 90 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 91 | 92 | if whoami.Type != "user" { 93 | t.Fatalf("Type is %v (should be user)", whoami.Type) 94 | } 95 | 96 | if whoami.Name != "ben" { 97 | t.Fatalf("Name is %v (should be ben)", whoami.Name) 98 | } 99 | 100 | if whoami.RoleSessionName != nil { 101 | t.Fatalf("RoleSessionName is set (to %v)", *whoami.RoleSessionName) 102 | } 103 | 104 | if whoami.SSOPermissionSet != nil { 105 | t.Fatalf("SSOPermissionSet is set (to %v)", *whoami.SSOPermissionSet) 106 | } 107 | } 108 | 109 | func TestAssumedRoleArn(t *testing.T) { 110 | account := "123456789012" 111 | arn := "arn:aws:iam::123456789012:assumed-role/ben/my-session" 112 | userId := "AROAJQABLZS4A3QDU576Q" 113 | 114 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 115 | 116 | whoami := Whoami{} 117 | 118 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 119 | 120 | if whoami.Type != "assumed-role" { 121 | t.Fatalf("Type is %v (should be assumed-role)", whoami.Type) 122 | } 123 | 124 | if whoami.Name != "ben" { 125 | t.Fatalf("Name is %v (should be ben)", whoami.Name) 126 | } 127 | 128 | if whoami.RoleSessionName == nil { 129 | t.Fatalf("RoleSessionName is not set") 130 | } 131 | 132 | if *whoami.RoleSessionName != "my-session" { 133 | t.Fatalf("RoleSessionName is %v (should be my-session)", whoami.RoleSessionName) 134 | } 135 | 136 | if whoami.SSOPermissionSet != nil { 137 | t.Fatalf("SSOPermissionSet is set (to %v)", *whoami.SSOPermissionSet) 138 | } 139 | } 140 | 141 | func TestSSOArn(t *testing.T) { 142 | account := "123456789012" 143 | arn := "arn:aws:iam::123456789012:assumed-role/AWSReservedSSO_SsoRole_abc123/ben" 144 | userId := "AROAJQABLZS4A3QDU576Q" 145 | 146 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 147 | 148 | whoami := Whoami{} 149 | 150 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 151 | 152 | if whoami.Type != "assumed-role" { 153 | t.Fatalf("Type is %v (should be assumed-role)", whoami.Type) 154 | } 155 | 156 | if whoami.Name != "AWSReservedSSO_SsoRole_abc123" { 157 | t.Fatalf("Name is %v (should be AWSReservedSSO_SsoRole_abc123)", whoami.Name) 158 | } 159 | 160 | if whoami.RoleSessionName == nil { 161 | t.Fatalf("RoleSessionName is not set") 162 | } 163 | 164 | if *whoami.RoleSessionName != "ben" { 165 | t.Fatalf("RoleSessionName is %v (should be ben)", whoami.RoleSessionName) 166 | } 167 | 168 | if whoami.SSOPermissionSet == nil { 169 | t.Fatalf("SSOPermissionSet is not set") 170 | } 171 | 172 | if *whoami.SSOPermissionSet != "SsoRole" { 173 | t.Fatalf("SSOPermissionSet is %v (should be SsoRole)", *whoami.SSOPermissionSet) 174 | } 175 | } 176 | 177 | func TestFederatedUser(t *testing.T) { 178 | account := "123456789012" 179 | arn := "arn:aws:iam::123456789012:federated-user/ben" 180 | userId := "AIDAJQABLZS4A3QDU576Q" 181 | 182 | getCallerIdentityOutput := NewGetCallerIdentityOutput(account, arn, userId) 183 | 184 | whoami := Whoami{} 185 | 186 | populateWhoamiFromGetCallerIdentityOutput(&whoami, getCallerIdentityOutput) 187 | 188 | if whoami.Type != "federated-user" { 189 | t.Fatalf("Type is %v (should be federated-user)", whoami.Type) 190 | } 191 | 192 | if whoami.Name != "ben" { 193 | t.Fatalf("Name is %v (should be ben)", whoami.Name) 194 | } 195 | 196 | if whoami.RoleSessionName != nil { 197 | t.Fatalf("RoleSessionName is set (to %v)", *whoami.RoleSessionName) 198 | } 199 | 200 | if whoami.SSOPermissionSet != nil { 201 | t.Fatalf("SSOPermissionSet is set (to %v)", *whoami.SSOPermissionSet) 202 | } 203 | } 204 | 205 | func TestDisableAccount(t *testing.T) { 206 | whoami := newWhoami( 207 | "123456789012", "arn:aws:iam::123456789012:assumed-role/ben/my-session", "AROAJQABLZS4A3QDU576Q", nil, "us-east-1") 208 | 209 | params := WhoamiParams{false, nil} 210 | 211 | if params.GetDisableAccountAlias(whoami) { 212 | t.Fatalf("Disabled when it shouldn't be") 213 | } 214 | 215 | params = WhoamiParams{true, nil} 216 | 217 | if !params.GetDisableAccountAlias(whoami) { 218 | t.Fatalf("Not disabled when it should be") 219 | } 220 | 221 | params = WhoamiParams{true, []string{"4444", "1234"}} 222 | 223 | // * The beginning or end of the account number 224 | // * The principal name or ARN 225 | // * The role session name 226 | if !params.GetDisableAccountAlias(whoami) { 227 | t.Fatalf("Should have been disabled by matching account prefix") 228 | } 229 | 230 | params = WhoamiParams{true, []string{"4444", "9012"}} 231 | if !params.GetDisableAccountAlias(whoami) { 232 | t.Fatalf("Should have been disabled by matching account suffix") 233 | } 234 | 235 | params = WhoamiParams{true, []string{"foo", "ben", "bar"}} 236 | if !params.GetDisableAccountAlias(whoami) { 237 | t.Fatalf("Should have been disabled by matching role name") 238 | } 239 | 240 | params = WhoamiParams{true, []string{"foo", "my-session", "bar"}} 241 | if !params.GetDisableAccountAlias(whoami) { 242 | t.Fatalf("Should have been disabled by matching role session name") 243 | } 244 | 245 | params = WhoamiParams{true, []string{"foo", "arn:aws:iam::123456789012:assumed-role/ben/my-session", "bar"}} 246 | if !params.GetDisableAccountAlias(whoami) { 247 | t.Fatalf("Should have been disabled by matching role ARN") 248 | } 249 | 250 | whoami = newWhoami( 251 | "123456789012", "arn:aws:iam::123456789012:assumed-role/AWSReservedSSO_SsoRole_abc123/benjamin", "AROAJQABLZS4A3QDU576Q", []string{"account-alias"}, "us-east-1") 252 | 253 | params = WhoamiParams{true, []string{"foo", "SsoRole", "bar"}} 254 | if !params.GetDisableAccountAlias(whoami) { 255 | t.Fatalf("Should have been disabled by matching SSO permission set") 256 | } 257 | 258 | params = WhoamiParams{true, []string{"foo", "benjamin", "bar"}} 259 | if !params.GetDisableAccountAlias(whoami) { 260 | t.Fatalf("Should have been disabled by matching role session name") 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /aws-whoami/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ben Kehoe 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "errors" 21 | "flag" 22 | "fmt" 23 | "os" 24 | "strings" 25 | 26 | "github.com/aws/aws-sdk-go-v2/aws" 27 | "github.com/aws/aws-sdk-go-v2/config" 28 | "github.com/aws/aws-sdk-go-v2/service/iam" 29 | "github.com/aws/aws-sdk-go-v2/service/sts" 30 | 31 | "github.com/aws/smithy-go" 32 | ) 33 | 34 | var Version string = "2.6" 35 | var DisableAccountAliasEnvVarName = "AWS_WHOAMI_DISABLE_ACCOUNT_ALIAS" 36 | 37 | type Whoami struct { 38 | Account string 39 | AccountAliases []string 40 | Arn string 41 | Type string 42 | Name string 43 | RoleSessionName *string 44 | UserId string 45 | Region string 46 | SSOPermissionSet *string 47 | } 48 | 49 | type WhoamiParams struct { 50 | DisableAccountAlias bool 51 | DisableAccountAliasValues []string 52 | } 53 | 54 | func NewWhoamiParams() WhoamiParams { 55 | var params WhoamiParams 56 | disableAccountAliasValue := os.Getenv(DisableAccountAliasEnvVarName) 57 | populateDisableAccountAlias(¶ms, disableAccountAliasValue) 58 | return params 59 | } 60 | 61 | func populateDisableAccountAlias(params *WhoamiParams, disableAccountAliasValue string) { 62 | switch strings.ToLower(disableAccountAliasValue) { 63 | case "": 64 | fallthrough 65 | case "0": 66 | fallthrough 67 | case "false": 68 | params.DisableAccountAlias = false 69 | params.DisableAccountAliasValues = nil 70 | return 71 | case "1": 72 | fallthrough 73 | case "true": 74 | params.DisableAccountAlias = true 75 | params.DisableAccountAliasValues = nil 76 | return 77 | default: 78 | accounts := strings.Split(disableAccountAliasValue, ",") 79 | if len(accounts) > 0 { 80 | params.DisableAccountAlias = true 81 | params.DisableAccountAliasValues = accounts 82 | return 83 | } else { 84 | params.DisableAccountAlias = false 85 | params.DisableAccountAliasValues = nil 86 | return 87 | } 88 | } 89 | } 90 | 91 | func (params WhoamiParams) GetDisableAccountAlias(whoami Whoami) bool { 92 | if !params.DisableAccountAlias { 93 | return false 94 | } 95 | if params.DisableAccountAliasValues == nil { 96 | return true 97 | } 98 | for _, disabledValue := range params.DisableAccountAliasValues { 99 | if strings.HasPrefix(whoami.Account, disabledValue) || strings.HasSuffix(whoami.Account, disabledValue) { 100 | return true 101 | } 102 | if whoami.Arn == disabledValue || whoami.Name == disabledValue { 103 | return true 104 | } 105 | if whoami.RoleSessionName != nil && *whoami.RoleSessionName == disabledValue { 106 | return true 107 | } 108 | if whoami.SSOPermissionSet != nil && *whoami.SSOPermissionSet == disabledValue { 109 | return true 110 | } 111 | } 112 | return false 113 | } 114 | 115 | func populateWhoamiFromGetCallerIdentityOutput(whoami *Whoami, getCallerIdentityOutput sts.GetCallerIdentityOutput) error { 116 | whoami.Account = *getCallerIdentityOutput.Account 117 | whoami.Arn = *getCallerIdentityOutput.Arn 118 | whoami.UserId = *getCallerIdentityOutput.UserId 119 | 120 | arnFields := strings.Split(whoami.Arn, ":") 121 | 122 | var arnResourceFields []string 123 | if arnFields[len(arnFields)-1] == "root" { 124 | arnResourceFields = []string{"root", "root"} 125 | } else { 126 | arnResourceFields = strings.SplitN(arnFields[len(arnFields)-1], "/", 2) 127 | if len(arnResourceFields) < 2 { 128 | return fmt.Errorf("arn %v has an unknown format", whoami.Arn) 129 | } 130 | } 131 | 132 | whoami.Type = arnResourceFields[0] 133 | if whoami.Type == "assumed-role" { 134 | nameFields := strings.SplitN(arnResourceFields[1], "/", 2) 135 | if len(arnResourceFields) < 2 { 136 | return fmt.Errorf("arn %v has an unknown format", whoami.Arn) 137 | } 138 | whoami.Name = nameFields[0] 139 | whoami.RoleSessionName = &nameFields[1] 140 | } else if whoami.Type == "user" { 141 | nameFields := strings.Split(arnResourceFields[1], "/") 142 | whoami.Name = nameFields[len(nameFields)-1] 143 | } else { 144 | whoami.Name = arnResourceFields[1] 145 | } 146 | 147 | if whoami.Type == "assumed-role" && strings.HasPrefix(whoami.Name, "AWSReservedSSO") { 148 | nameFields := strings.Split(whoami.Name, "_") 149 | if len(nameFields) >= 3 { 150 | permSetStr := strings.Join(nameFields[1:len(nameFields)-1], "_") 151 | whoami.SSOPermissionSet = &permSetStr 152 | } 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func NewWhoami(awsConfig aws.Config, params WhoamiParams) (Whoami, error) { 159 | stsClient := sts.NewFromConfig(awsConfig) 160 | 161 | getCallerIdentityOutput, err := stsClient.GetCallerIdentity(context.TODO(), nil) 162 | 163 | if err != nil { 164 | return Whoami{}, err 165 | } 166 | 167 | var whoami Whoami 168 | whoami.AccountAliases = make([]string, 0, 1) 169 | 170 | whoami.Region = awsConfig.Region 171 | 172 | err = populateWhoamiFromGetCallerIdentityOutput(&whoami, *getCallerIdentityOutput) 173 | 174 | if err != nil { 175 | return whoami, err 176 | } 177 | 178 | if !params.GetDisableAccountAlias(whoami) { 179 | iam_client := iam.NewFromConfig(awsConfig) 180 | 181 | // pedantry 182 | paginator := iam.NewListAccountAliasesPaginator(iam_client, nil) 183 | 184 | for paginator.HasMorePages() { 185 | output, err := paginator.NextPage(context.TODO()) 186 | if err != nil { 187 | var apiErr smithy.APIError 188 | if errors.As(err, &apiErr) && apiErr.ErrorCode() == "AccessDenied" { 189 | break 190 | } else { 191 | return whoami, err 192 | } 193 | } 194 | whoami.AccountAliases = append(whoami.AccountAliases, output.AccountAliases...) 195 | } 196 | } 197 | 198 | return whoami, nil 199 | } 200 | 201 | type record struct { 202 | field string 203 | value string 204 | } 205 | 206 | func getTypeNameRecord(whoami Whoami) record { 207 | if whoami.Type == "root" { 208 | return record{"Type: ", "root"} 209 | } 210 | fields := strings.Split(whoami.Type, "-") 211 | typeParts := make([]string, 0, 3) 212 | for _, field := range fields { 213 | s := strings.ToUpper(field[:1]) + field[1:] // ok because always ASCII 214 | typeParts = append(typeParts, s) 215 | } 216 | typeParts = append(typeParts, ": ") 217 | return record{strings.Join(typeParts, ""), whoami.Name} 218 | } 219 | 220 | func (whoami Whoami) Format() string { 221 | records := make([]record, 0, 7) 222 | records = append(records, record{"Account: ", whoami.Account}) 223 | for _, alias := range whoami.AccountAliases { 224 | records = append(records, record{"", alias}) 225 | } 226 | records = append(records, record{"Region: ", whoami.Region}) 227 | if whoami.SSOPermissionSet != nil { 228 | records = append(records, record{"AWS SSO: ", *whoami.SSOPermissionSet}) 229 | } else { 230 | records = append(records, getTypeNameRecord(whoami)) 231 | } 232 | if whoami.RoleSessionName != nil { 233 | records = append(records, record{"RoleSessionName: ", *whoami.RoleSessionName}) 234 | } 235 | records = append(records, record{"UserId: ", whoami.UserId}) 236 | records = append(records, record{"Arn: ", whoami.Arn}) 237 | 238 | var maxLen int = 0 239 | for _, rec := range records { 240 | if len(rec.field) > maxLen { 241 | maxLen = len(rec.field) 242 | } 243 | } 244 | 245 | lines := make([]string, 0, 7) 246 | for _, rec := range records { 247 | lines = append(lines, rec.field+strings.Repeat(" ", maxLen-len(rec.field))+rec.value) 248 | } 249 | 250 | return strings.Join(lines, "\n") 251 | } 252 | 253 | type errorRecord struct { 254 | Error string 255 | } 256 | 257 | func printError(err error, useJson bool) { 258 | if useJson { 259 | bytes, err := json.Marshal(errorRecord{err.Error()}) 260 | if err != nil { 261 | fmt.Fprintln(os.Stderr, "{\"Error\": \"Failed to serialize error\"}") 262 | } else { 263 | fmt.Fprintln(os.Stderr, string(bytes)) 264 | } 265 | } else { 266 | fmt.Fprintln(os.Stderr, err.Error()) 267 | } 268 | } 269 | 270 | func main() { 271 | profile := flag.String("profile", "", "A config profile to use") 272 | useJson := flag.Bool("json", false, "Output as JSON") 273 | disableAccountAlias := flag.Bool("disable-account-alias", false, "Disable account alias check") 274 | showVersion := flag.Bool("version", false, "Display the version") 275 | flag.Parse() 276 | 277 | if *showVersion { 278 | fmt.Println(Version) 279 | return 280 | } 281 | 282 | awsConfig, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(*profile)) 283 | if err != nil { 284 | printError(err, *useJson) 285 | os.Exit(1) 286 | } 287 | 288 | whoamiParams := NewWhoamiParams() 289 | 290 | if *disableAccountAlias { 291 | whoamiParams.DisableAccountAlias = true 292 | whoamiParams.DisableAccountAliasValues = nil 293 | } 294 | 295 | Whoami, err := NewWhoami(awsConfig, whoamiParams) 296 | 297 | if err != nil { 298 | printError(err, *useJson) 299 | os.Exit(1) 300 | } 301 | 302 | if *useJson { 303 | bytes, err := json.Marshal(Whoami) 304 | if err != nil { 305 | printError(err, *useJson) 306 | os.Exit(1) 307 | } 308 | fmt.Println(string(bytes)) 309 | } else { 310 | fmt.Println(Whoami.Format()) 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Ben Kehoe 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------