├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── aws_cli.go ├── aws_eks.go ├── aws_sso.go ├── aws_sso_test.go ├── config.go ├── file.go ├── file_test.go ├── go.mod ├── go.sum ├── goreleaser.yml ├── main.go ├── util.go ├── util_test.go └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - leocomelli 11 | assignees: 12 | - leocomelli 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test-build: 9 | name: Test & Build 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Set up Go 1.18 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: '1.18.4' 17 | 18 | - name: Set GOPATH and PATH 19 | run: | 20 | echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV 21 | echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH 22 | shell: bash 23 | 24 | - name: Check out code 25 | uses: actions/checkout@v2 26 | 27 | - name: Update build dependencies 28 | run: make setup 29 | 30 | - name: Check quality code 31 | run: make lint 32 | 33 | - name: Test 34 | run: make test 35 | 36 | - name: Build 37 | run: make bin 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build: 8 | name: Create Release 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Set up Go 1.18 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: '1.18.4' 16 | 17 | - name: Set GOPATH and PATH 18 | run: | 19 | echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV 20 | echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH 21 | shell: bash 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v2 25 | 26 | - name: Generate releases 27 | uses: goreleaser/goreleaser-action@v2 28 | with: 29 | version: latest 30 | args: release --rm-dist 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage.txt 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Leonardo Comelli 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 | #!/usr/bin/make 2 | 3 | .DEFAULT_GOAL := all 4 | 5 | .PHONY: setup 6 | setup: 7 | @go install github.com/securego/gosec/v2/cmd/gosec@v2.12.0 8 | @go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.2 9 | 10 | .PHONY: lint 11 | lint: 12 | golangci-lint run 13 | 14 | .PHONY: sec 15 | sec: 16 | @gosec -exclude=G401,G204,G505,G101 -quiet ./... 17 | 18 | .PHONY: bin 19 | bin: 20 | @go build -o ./dist/asl 21 | 22 | .PHONY: test 23 | test: 24 | @go test ./... -race -coverprofile=coverage.txt -covermode=atomic 25 | 26 | .PHONY: all 27 | all: 28 | @make -s bin test lint sec 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASL ::: Amazon Single Sign-On Login 2 | 3 | ASL is a cli to get the STS short-term credentials for all accounts and role names that is assigned to the AWS SSO user. 4 | 5 | ## What does ASL do? 6 | 7 | ASL retrieves and caches an AWS SSO access token to exchange for AWS credentials, when the cached access token expires, a new login is requested. Using a valid access token, the ASL lists all AWS accounts assigned to the user and then get the roles for each one. After that, the STS short-term credentials are stored in AWS credential file. 8 | 9 | ## Prerequisites 10 | 11 | * [AWS Command Line Interface](https://aws.amazon.com/cli/) 12 | 13 | ## Installation 14 | 15 | ```sh 16 | sudo bash -c "curl -fsSL https://github.com/leocomelli/asl/releases/latest/download/asl_$(uname -s)_$(uname -m) -o /usr/local/bin/asl && chmod +x /usr/local/bin/asl" 17 | ``` 18 | 19 | ## Usage 20 | 21 | Run the `asl configure` command to store the AWS SSO Login parameters to be used when needed. Whenever the AWS SSO access token needs to be renewed, these parameters are used. 22 | 23 | ```sh 24 | asl configure \ 25 | --account-id 123456789012 \ 26 | --start-url https://d-123456w78w.awsapps.com/start/ \ 27 | --role-name MyRoleSSOLogin \ 28 | --region us-east-1 29 | ``` 30 | 31 | Run the `asl` command to store the STS short-term credentials for each account and role assigned to the user. You may safely rerun the `asl` command to refresh your credentials. 32 | 33 | ```sh 34 | asl 35 | ``` 36 | 37 | Make sure everything works well 38 | 39 | ```sh 40 | aws sts get-caller-identity --profile your-profile 41 | ``` 42 | 43 | ### EKS 44 | 45 | Use the flag `--eks` to update the kubeconfig with all existing clusters in the accounts assigned to the user. 46 | 47 | ```sh 48 | asl --eks 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /aws_cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | 9 | logger "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // SSOCommand represents the commands for interacting with AWS SSO 13 | type SSOCommand interface { 14 | Login(string) (string, error) 15 | ListAccounts(string, string) (string, error) 16 | ListAccountRoles(string, string, string) (string, error) 17 | GetRoleCredentials(string, string, string, string) (string, error) 18 | } 19 | 20 | // EKSCommand represents the commands for interacting with EKS 21 | type EKSCommand interface { 22 | ListClusters(string, string) (string, error) 23 | UpdateKubeConfig(string, string, string) (string, error) 24 | } 25 | 26 | // ----- SSO ----- 27 | 28 | // SSOCli implements commands to perform SSO actions through AWS Cli 29 | type SSOCli struct{} 30 | 31 | // Login retrieves and caches an AWS SSO access token to exchange for AWS credentials 32 | func (c *SSOCli) Login(roleName string) (string, error) { 33 | return execCli("sso", "login", "--profile", roleName) 34 | } 35 | 36 | // ListAccounts lists all AWS accounts assigned to the user 37 | func (c *SSOCli) ListAccounts(accessToken string, region string) (string, error) { 38 | return execCli("sso", "list-accounts", "--access-token", accessToken, "--region", region) 39 | } 40 | 41 | // ListAccountRoles lists all roles that are assigned to the user for a given AWS account 42 | func (c *SSOCli) ListAccountRoles(accessToken string, region string, accountID string) (string, error) { 43 | return execCli("sso", "list-account-roles", "--access-token", accessToken, "--region", region, "--account-id", accountID) 44 | } 45 | 46 | // GetRoleCredentials returns the STS short-term credentials for a given role name that is assigned to the user 47 | func (c *SSOCli) GetRoleCredentials(accessToken string, region string, accountID string, roleName string) (string, error) { 48 | return execCli("sso", "get-role-credentials", "--access-token", accessToken, "--region", region, "--account-id", accountID, "--role-name", roleName) 49 | } 50 | 51 | // ----- EKS ----- 52 | 53 | // EKSCli implements commands to perform EKS actions through AWS Cli 54 | type EKSCli struct{} 55 | 56 | // ListClusters lists the Amazon EKS clusters in your AWS account in the specified region 57 | func (k *EKSCli) ListClusters(region string, profile string) (string, error) { 58 | return execCli("eks", "list-clusters", "--region", region, "--profile", profile) 59 | } 60 | 61 | // UpdateKubeConfig configures kubectl so that you can connect to an Amazon EKS cluster 62 | func (k *EKSCli) UpdateKubeConfig(region string, profile string, name string) (string, error) { 63 | return execCli("eks", "update-kubeconfig", "--name", name, "--region", region, "--profile", profile) 64 | } 65 | 66 | func execCli(args ...string) (string, error) { 67 | cmd := exec.Command("aws", args...) 68 | out, err := cmd.CombinedOutput() 69 | outStr := strings.ReplaceAll(string(out), "\n", "") 70 | 71 | logger.Trace().Interface("command", args).Msg(strings.ReplaceAll(outStr, "\n", "")) 72 | 73 | var ee *exec.ExitError 74 | if errors.As(err, &ee) && ee.ExitCode() != 0 { 75 | return "", fmt.Errorf("[aws cli] %s", outStr) 76 | } 77 | 78 | return outStr, err 79 | } 80 | -------------------------------------------------------------------------------- /aws_eks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "path/filepath" 6 | 7 | "github.com/mitchellh/go-homedir" 8 | logger "github.com/rs/zerolog/log" 9 | ) 10 | 11 | var kubeConfig string 12 | 13 | // EKSClusters defines the structure returned by AWS Cli 14 | type EKSClusters struct { 15 | Items []string `json:"clusters"` 16 | } 17 | 18 | // EKS implements the flow to retrieve the EKS clusters configuration to use them with kubectl 19 | type EKS struct { 20 | Cmd EKSCommand 21 | KubeConfigPath string 22 | BackupFile bool 23 | } 24 | 25 | // NewEKS returns a new EKS 26 | func NewEKS(cmd EKSCommand, c *ConfigOptions) *EKS { 27 | return &EKS{ 28 | Cmd: cmd, 29 | KubeConfigPath: kubeConfig, 30 | BackupFile: c.BackupFile, 31 | } 32 | } 33 | 34 | // UpdateKubeConfig constructs a configuration with prepopulated server and certificate 35 | // authority data values for each credential retrieved. 36 | func (e *EKS) UpdateKubeConfig(creds []*Credential) error { 37 | if e.BackupFile { 38 | kubeConfigFile := NewFile(e.KubeConfigPath) 39 | filename, err := kubeConfigFile.Backup() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | logger.Info().Str("path", filename).Msg("backup completed successfully") 45 | } 46 | 47 | for _, cred := range creds { 48 | out, err := e.Cmd.ListClusters(cred.Region, cred.ProfileName) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | logger.Debug().Str("profile", cred.ProfileName).Str("region", cred.Region).Msg("listing eks clusters...") 54 | 55 | clusters := &EKSClusters{} 56 | err = json.Unmarshal([]byte(out), clusters) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | logger.Debug().Msgf("%d clusters were found", len(clusters.Items)) 62 | 63 | for _, c := range clusters.Items { 64 | out, err := e.Cmd.UpdateKubeConfig(cred.Region, cred.ProfileName, c) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | logger.Info().Str("cluster", c).Str("profile", cred.ProfileName).Msg("kubeconfig successfully updated") 70 | 71 | logger.Trace().Str("cluster", c).Msg(out) 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func init() { 79 | home, err := homedir.Dir() 80 | if err != nil { 81 | logger.Fatal().Err(err) 82 | } 83 | 84 | kubeConfig = filepath.Join(home, ".kube", "config") 85 | } 86 | -------------------------------------------------------------------------------- /aws_sso.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "time" 13 | 14 | "github.com/mitchellh/go-homedir" 15 | logger "github.com/rs/zerolog/log" 16 | "gopkg.in/ini.v1" 17 | ) 18 | 19 | const ( 20 | awsSSOPath = "sso" 21 | keyRegion = "region" 22 | keySSOUrl = "sso_start_url" 23 | keySSORegion = "sso_region" 24 | keySSOAccountID = "sso_account_id" 25 | keySSORoleName = "sso_role_name" 26 | keyCrdAccessKeyID = "aws_access_key_id" 27 | keyCrdSecretAccessKey = "aws_secret_access_key" 28 | keyCrdSessionToken = "aws_session_token" 29 | ) 30 | 31 | var awsPath string 32 | 33 | // SSOCredential defines the structure returned by AWS Cli 34 | type SSOCredential struct { 35 | URL string `json:"startUrl"` 36 | Region string `json:"region"` 37 | AccessToken string `json:"accessToken"` 38 | ExpiresAsStr string `json:"expiresAt"` 39 | } 40 | 41 | // SSO implements the flow to retrieve the AWS SSO credentials 42 | type SSO struct { 43 | Cmd SSOCommand `json:"-"` 44 | AccountID string `json:"accountId"` 45 | RoleName string `json:"roleName"` 46 | StartURL string `json:"startUrl"` 47 | Region string `json:"region"` 48 | BackupFile bool `json:"-"` 49 | ForceSSOLogin bool `json:"-"` 50 | } 51 | 52 | // Accounts defines the structure returned by AWS Cli 53 | type Accounts struct { 54 | Items []*Account `json:"accountList"` 55 | } 56 | 57 | // Account defines the structure returned by AWS Cli 58 | type Account struct { 59 | ID string `json:"accountId"` 60 | Name string `json:"accountName"` 61 | Email string `json:"emailAddress"` 62 | Roles []string `json:"-"` 63 | } 64 | 65 | // AccountRoles defines the structure returned by AWS Cli 66 | type AccountRoles struct { 67 | Items []*AccountRole `json:"roleList"` 68 | } 69 | 70 | // AccountRole defines the structure returned by AWS Cli 71 | type AccountRole struct { 72 | RoleName string `json:"roleName"` 73 | AccountID string `json:"accountId"` 74 | } 75 | 76 | // Credentials defines the structure returned by AWS Cli 77 | type Credentials struct { 78 | Item *Credential `json:"roleCredentials"` 79 | } 80 | 81 | // Credential defines the structure returned by AWS Cli 82 | type Credential struct { 83 | ProfileName string `json:"-"` 84 | AccountName string `json:"-"` 85 | Region string `json:"-"` 86 | AccessKeyID string `json:"accessKeyId"` 87 | SecretAccessKey string `json:"secretAccessKey"` 88 | SessionToken string `json:"sessionToken"` 89 | Expiration int64 `json:"expiration"` 90 | } 91 | 92 | // CredentialResultInfo defines the information about SSO credentials 93 | type CredentialResultInfo struct { 94 | Filename string 95 | ExpiresAt time.Time 96 | } 97 | 98 | // NewSSO returns a new SSO 99 | func NewSSO(cmd SSOCommand, c *ConfigOptions) *SSO { 100 | return &SSO{ 101 | cmd, 102 | c.AccountID, 103 | c.RoleName, 104 | c.StartURL, 105 | c.Region, 106 | c.BackupFile, 107 | c.ForceSSOLogin, 108 | } 109 | } 110 | 111 | // List returns a slice of account roles 112 | func (r *AccountRoles) List() []string { 113 | var list []string 114 | for _, r := range r.Items { 115 | list = append(list, r.RoleName) 116 | } 117 | return list 118 | } 119 | 120 | // ExpiresAt parses the expiration date 121 | // There is a workaround because the aws cli stores the expiration date 122 | // in different formats. 123 | func (c *SSOCredential) ExpiresAt() time.Time { 124 | // aws-cli/2.1.29 Python/3.8.8 Darwin/20.3.0 exe/x86_64 prompt/off 125 | layout := "2006-01-02T15:04:05Z" 126 | t, err := time.Parse(layout, c.ExpiresAsStr) 127 | if err != nil { 128 | //aws-cli/2.0.40 Python/3.8.5 Darwin/19.6.0 source/x86_64 129 | layout = "2006-01-02T15:04:05UTC" 130 | t, err = time.Parse(layout, c.ExpiresAsStr) 131 | if err != nil { 132 | logger.Warn().Str("value", c.ExpiresAsStr).Msg("error parsing date") 133 | return time.Now().UTC().AddDate(0, 0, -1) 134 | } 135 | } 136 | 137 | return t 138 | } 139 | 140 | // Expired returns if credentials are expired 141 | func (c *SSOCredential) Expired() bool { 142 | return time.Now().UTC().After(c.ExpiresAt()) 143 | } 144 | 145 | // ExpiresAt returns the expiratin date 146 | func (d *Credential) ExpiresAt() time.Time { 147 | return time.Unix(d.Expiration/1000, 0) 148 | } 149 | 150 | // PersistConfig writes the sso config file 151 | func (a *SSO) PersistConfig() error { 152 | config := NewFile(awsPath, "config") 153 | 154 | logger.Debug().Str("path", config.FullName).Msg("preparing to store the aws sso config file") 155 | 156 | // create file on disk when it does not exist 157 | if err := config.Create(); err != nil { 158 | return err 159 | } 160 | 161 | if a.BackupFile { 162 | filename, err := config.Backup() 163 | if err != nil { 164 | return err 165 | } 166 | 167 | logger.Info().Str("path", filename).Msg("backup completed successfully") 168 | } 169 | 170 | cfg, _ := ini.LooseLoad(config.FullName) 171 | 172 | s := cfg.Section(fmt.Sprintf("profile %s", a.RoleName)) 173 | s.Key("output").SetValue("json") 174 | s.Key(keyRegion).SetValue(a.Region) 175 | s.Key(keySSOUrl).SetValue(a.StartURL) 176 | s.Key(keySSORegion).SetValue(a.Region) 177 | s.Key(keySSOAccountID).SetValue(a.AccountID) 178 | s.Key(keySSORoleName).SetValue(a.RoleName) 179 | 180 | if err := cfg.SaveTo(config.FullName); err != nil { 181 | return err 182 | } 183 | 184 | logger.Info().Str("path", config.FullName).Msg("the aws sso config file has been successfully stored") 185 | 186 | return nil 187 | } 188 | 189 | func (a *SSO) loginRetry(v []bool) bool { 190 | return len(v) > 0 && v[0] 191 | } 192 | 193 | // Login checks if the sso cache file is valid, 194 | // when cache credential has expired forces a login 195 | func (a *SSO) Login(retry ...bool) (*SSOCredential, error) { 196 | if a.ForceSSOLogin || a.loginRetry(retry) { 197 | _, err := a.Cmd.Login(a.RoleName) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | logger.Info().Msg("the aws sso cache file has been updated successfully") 203 | } 204 | 205 | c, err := a.ReadCacheFile() 206 | 207 | if err != nil && !os.IsNotExist(err) { 208 | return nil, err 209 | } 210 | 211 | if c != nil && !c.Expired() { 212 | logger.Debug().Bool("expired", c.Expired()).Time("expiresAt", c.ExpiresAt()).Msg("the sso cache file token is not expired") 213 | return c, nil 214 | } 215 | 216 | if a.loginRetry(retry) { 217 | return nil, errors.New("can not renew the sso token") 218 | } 219 | 220 | return a.Login(true) 221 | } 222 | 223 | // ListAccounts lists accounts assigned to the user 224 | func (a *SSO) ListAccounts(c *SSOCredential) ([]*Account, error) { 225 | 226 | out, err := a.Cmd.ListAccounts(c.AccessToken, c.Region) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | accounts := &Accounts{} 232 | err = json.Unmarshal([]byte(out), accounts) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | logger.Debug().Interface("accounts", accounts).Msg("accounts obtained with credentials") 238 | 239 | for _, account := range accounts.Items { 240 | out, err := a.Cmd.ListAccountRoles(c.AccessToken, c.Region, account.ID) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | roles := &AccountRoles{} 246 | err = json.Unmarshal([]byte(out), roles) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | logger.Debug().Interface("roles", roles).Str("accountID", account.ID).Msg("roles by account") 252 | 253 | account.Roles = roles.List() 254 | } 255 | 256 | return accounts.Items, nil 257 | } 258 | 259 | // GetCredentials retrieves the credentials of the account assigned to the user 260 | func (a *SSO) GetCredentials(c *SSOCredential, accounts []*Account) ([]*Credential, error) { 261 | var creds []*Credential 262 | for _, acc := range accounts { 263 | for i, r := range acc.Roles { 264 | out, err := a.Cmd.GetRoleCredentials(c.AccessToken, c.Region, acc.ID, r) 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | cs := &Credentials{} 270 | if err := json.Unmarshal([]byte(out), cs); err != nil { 271 | return nil, err 272 | } 273 | 274 | logger.Debug().Interface("credentials", cs).Str("accountID", acc.ID).Str("role", r).Msg("credentials...") 275 | 276 | profile := strings.ReplaceAll(acc.Name, " ", "-") 277 | if i > 0 { 278 | profile = fmt.Sprintf("%s-%s", profile, Snake(r)) 279 | } 280 | 281 | cs.Item.AccountName = acc.Name 282 | cs.Item.Region = c.Region 283 | cs.Item.ProfileName = strings.ToLower(profile) 284 | 285 | logger.Info().Str("account", acc.Name).Str("region", c.Region).Msgf("credentials profile %s", cs.Item.ProfileName) 286 | 287 | creds = append(creds, cs.Item) 288 | } 289 | } 290 | 291 | logger.Debug().Msgf("%d credentials have been generated", len(creds)) 292 | 293 | if len(creds) == 0 { 294 | return nil, errors.New("no credentials were found") 295 | } 296 | 297 | return creds, nil 298 | } 299 | 300 | // PersistCredentials writes the credentials to the AWS file 301 | func (a *SSO) PersistCredentials(creds []*Credential) (*CredentialResultInfo, error) { 302 | cred := NewFile(awsPath, "credentials") 303 | 304 | if a.BackupFile { 305 | filename, err := cred.Backup() 306 | if err != nil { 307 | return nil, err 308 | } 309 | 310 | logger.Info().Str("path", filename).Msg("backup completed successfully") 311 | } 312 | 313 | cfg, _ := ini.LooseLoad(cred.FullName) 314 | 315 | for _, c := range creds { 316 | s := cfg.Section(c.ProfileName) 317 | s.Key("output").SetValue("json") 318 | s.Key(keyRegion).SetValue(c.Region) 319 | s.Key(keyCrdAccessKeyID).SetValue(c.AccessKeyID) 320 | s.Key(keyCrdSecretAccessKey).SetValue(c.SecretAccessKey) 321 | s.Key(keyCrdSessionToken).SetValue(c.SessionToken) 322 | } 323 | 324 | if err := cfg.SaveTo(cred.FullName); err != nil { 325 | return nil, err 326 | } 327 | 328 | return &CredentialResultInfo{ 329 | Filename: cred.FullName, 330 | ExpiresAt: creds[len(creds)-1].ExpiresAt(), 331 | }, nil 332 | } 333 | 334 | // ReadCacheFile reads the sso cache file for a given sso 335 | func (a *SSO) ReadCacheFile() (*SSOCredential, error) { 336 | hash := sha1.New() 337 | _, err := hash.Write([]byte(a.StartURL)) 338 | if err != nil { 339 | return nil, err 340 | } 341 | 342 | cacheFilename := strings.ToLower(hex.EncodeToString(hash.Sum(nil))) + ".json" 343 | cache := NewFile(awsPath, awsSSOPath, "cache", cacheFilename) 344 | 345 | logger.Debug().Str("path", cache.FullName).Msg("searching for the aws sso cache file...") 346 | 347 | if !cache.Exists() { 348 | logger.Debug().Str("path", cache.FullName).Msg("aws sso cache file not found") 349 | return nil, os.ErrNotExist 350 | } 351 | 352 | data := &SSOCredential{} 353 | if err := cache.ReadJSON(data); err != nil { 354 | return nil, err 355 | } 356 | 357 | logger.Debug().Interface("data", data).Msg("cache file was read successfully") 358 | 359 | return data, err 360 | } 361 | 362 | func init() { 363 | home, err := homedir.Dir() 364 | if err != nil { 365 | logger.Fatal().Err(err) 366 | } 367 | 368 | awsPath = filepath.Join(home, ".aws") 369 | } 370 | -------------------------------------------------------------------------------- /aws_sso_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | type SSOMock struct{} 6 | 7 | func (c *SSOMock) Login(roleName string) (string, error) { 8 | return "", nil 9 | } 10 | 11 | func (c *SSOMock) ListAccounts(accessToken string, region string) (string, error) { 12 | return "", nil 13 | } 14 | 15 | func (c *SSOMock) ListAccountRoles(accessToken string, region string, accountID string) (string, error) { 16 | return "", nil 17 | } 18 | 19 | func (c *SSOMock) GetRoleCredentials(accessToken string, region string, accountID string, roleName string) (string, error) { 20 | return "", nil 21 | } 22 | 23 | func TestX(t *testing.T) {} 24 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "path/filepath" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | logger "github.com/rs/zerolog/log" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var aslPath string 14 | 15 | // ConfigOptions defines the ASL options 16 | type ConfigOptions struct { 17 | AccountID string `json:"accountId"` 18 | RoleName string `json:"roleName"` 19 | StartURL string `json:"startUrl"` 20 | Region string `json:"region"` 21 | BackupFile bool `json:"-"` 22 | ForceSSOLogin bool `json:"-"` 23 | } 24 | 25 | func configureCmd(ctx context.Context) *cobra.Command { 26 | o := &ConfigOptions{} 27 | 28 | cmd := &cobra.Command{ 29 | Use: "configure", 30 | Short: "Store the parameters used to log in to AWS SSO", 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | logger.Debug().Str("aslPath", aslPath).Interface("options", o).Msg("configuring...") 33 | 34 | if err := Configure(o); err != nil { 35 | return err 36 | } 37 | 38 | logger.Info().Msg("it worked! please run: asl") 39 | 40 | return nil 41 | }, 42 | } 43 | 44 | cmd.Flags().StringVarP(&o.AccountID, "account-id", "a", "", "the AWS account that is assigned to the user") 45 | cmd.Flags().StringVarP(&o.RoleName, "role-name", "R", "", "the role name that is assigned to the user") 46 | cmd.Flags().StringVarP(&o.StartURL, "start-url", "u", "", "the URL that points to the organization's AWS Single Sign-On (AWS SSO) user portal") 47 | cmd.Flags().StringVarP(&o.Region, "region", "r", "", "the region to use") 48 | 49 | _ = cmd.MarkFlagRequired("account-id") 50 | _ = cmd.MarkFlagRequired("role-name") 51 | _ = cmd.MarkFlagRequired("start-url") 52 | _ = cmd.MarkFlagRequired("region") 53 | 54 | return cmd 55 | } 56 | 57 | // Configure writes the ASL parameters to use when needed 58 | func Configure(o *ConfigOptions) error { 59 | config := NewFile(aslPath) 60 | if err := config.WriteJSON(o); err != nil { 61 | return err 62 | } 63 | 64 | logger.Debug().Str("path", config.FullName).Msg("the asl config file has been successfully stored") 65 | 66 | return nil 67 | 68 | } 69 | 70 | // LoadConfig reads the ASL parameters 71 | func LoadConfig(opts *Options) (*ConfigOptions, error) { 72 | config := NewFile(aslPath) 73 | 74 | if !config.Exists() { 75 | return nil, errors.New("asl config file not found. please run: asl configure") 76 | } 77 | 78 | logger.Info().Str("path", config.FullName).Msg("loading the asl config file") 79 | 80 | data := &ConfigOptions{} 81 | err := config.ReadJSON(data) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | data.BackupFile = opts.Backup 87 | data.ForceSSOLogin = opts.ForceSSOLogin 88 | 89 | logger.Debug().Interface("data", data).Msg("the asl config file has been successfully read") 90 | 91 | return data, nil 92 | } 93 | 94 | func init() { 95 | home, err := homedir.Dir() 96 | if err != nil { 97 | logger.Fatal().Err(err) 98 | } 99 | 100 | aslPath = filepath.Join(home, ".asl") 101 | } 102 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "text/template" 10 | "time" 11 | ) 12 | 13 | const ( 14 | filePerm os.FileMode = 0600 15 | dirPerm os.FileMode = 0750 16 | ) 17 | 18 | // File represents the file options 19 | type File struct { 20 | Filename string 21 | Path string 22 | FullName string 23 | Extension string 24 | } 25 | 26 | // NewFile returns a new File 27 | func NewFile(path ...string) *File { 28 | p := filepath.Join(path...) 29 | return &File{ 30 | Filename: filepath.Base(p), 31 | Path: filepath.Dir(p), 32 | FullName: p, 33 | Extension: filepath.Ext(p), 34 | } 35 | } 36 | 37 | // Exists returns if file exists 38 | func (f *File) Exists() bool { 39 | _, err := os.Stat(f.FullName) 40 | return !os.IsNotExist(err) 41 | } 42 | 43 | // Create a directory, along with any necessary parents 44 | func (f *File) Create() error { 45 | if err := os.MkdirAll(f.Path, dirPerm); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // Backup makes a copy of the file 53 | func (f *File) Backup() (string, error) { 54 | if !f.Exists() { 55 | return "", nil 56 | } 57 | 58 | bkpFilename := fmt.Sprintf("%s.backup_%d", f.FullName, time.Now().Unix()) 59 | b, err := os.ReadFile(f.FullName) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | if err := os.WriteFile(bkpFilename, b, filePerm); err != nil { 65 | return "", err 66 | } 67 | 68 | return bkpFilename, nil 69 | } 70 | 71 | // Read reads a file and returns the content as []byte 72 | func (f *File) Read() ([]byte, error) { 73 | b, err := os.ReadFile(f.FullName) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return b, nil 79 | } 80 | 81 | // ReadString reads a file and returns the content as string 82 | func (f *File) ReadString() (string, error) { 83 | b, err := os.ReadFile(f.FullName) 84 | if err != nil { 85 | return "", err 86 | } 87 | 88 | return string(b), nil 89 | } 90 | 91 | // ReadJSON reads a file and fills in the provided struct 92 | func (f *File) ReadJSON(s interface{}) error { 93 | b, err := f.Read() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | err = json.Unmarshal(b, s) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | return nil 104 | } 105 | 106 | // Write writes the string content in the file 107 | func (f *File) Write(content string) error { 108 | return os.WriteFile(f.FullName, []byte(content), filePerm) 109 | } 110 | 111 | // WriteJSON writes the json in the file 112 | func (f *File) WriteJSON(data interface{}) error { 113 | b, err := json.MarshalIndent(data, "", " ") 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return os.WriteFile(f.FullName, b, filePerm) 119 | } 120 | 121 | // WriteTemplate writes the struct using a template 122 | func (f *File) WriteTemplate(tmpl string, data interface{}) error { 123 | t, err := template.New("tmpl").Parse(tmpl) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | var b bytes.Buffer 129 | if err = t.Execute(&b, data); err != nil { 130 | return err 131 | } 132 | 133 | return os.WriteFile(f.FullName, b.Bytes(), filePerm) 134 | } 135 | 136 | // WriteTemplateSlice writes a slice using a template 137 | func (f *File) WriteTemplateSlice(tmpl string, data []interface{}) error { 138 | t, err := template.New("tmpl").Parse(tmpl) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | var b bytes.Buffer 144 | for _, d := range data { 145 | if err = t.Execute(&b, d); err != nil { 146 | return err 147 | } 148 | } 149 | 150 | return os.WriteFile(f.FullName, b.Bytes(), filePerm) 151 | } 152 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type Foo struct { 13 | ID int 14 | Name string 15 | } 16 | 17 | const ( 18 | tmpDir = "/tmp" 19 | tmpFilename = "go-test" 20 | ) 21 | 22 | func createTempFile(data []byte) ([]string, func()) { 23 | f, _ := os.CreateTemp(tmpDir, tmpFilename) 24 | _, _ = f.Write(data) 25 | return filepath.SplitList(f.Name()), func() { 26 | os.Remove(f.Name()) 27 | } 28 | } 29 | 30 | func TestReadFileAsByteArray(t *testing.T) { 31 | content := ` 32 | line1 33 | line2 34 | line3 35 | ` 36 | path, r := createTempFile([]byte(content)) 37 | defer r() 38 | 39 | f := NewFile(path...) 40 | res, _ := f.Read() 41 | require.Equal(t, content, string(res)) 42 | } 43 | 44 | func TestReadFileAsString(t *testing.T) { 45 | content := ` 46 | line1 47 | line2 48 | line3 49 | ` 50 | path, r := createTempFile([]byte(content)) 51 | defer r() 52 | 53 | f := NewFile(path...) 54 | res, _ := f.ReadString() 55 | require.Equal(t, content, res) 56 | } 57 | 58 | func TestReadFileAsJSON(t *testing.T) { 59 | content := ` 60 | { 61 | "id": 1, 62 | "name": "foo" 63 | } 64 | ` 65 | path, r := createTempFile([]byte(content)) 66 | defer r() 67 | 68 | s := &Foo{} 69 | 70 | f := NewFile(path...) 71 | _ = f.ReadJSON(s) 72 | require.Equal(t, &Foo{ID: 1, Name: "foo"}, s) 73 | } 74 | 75 | func TestWriteTextFile(t *testing.T) { 76 | filename := "/tmp/go-test-text" 77 | defer os.Remove(filename) 78 | 79 | f := NewFile(filename) 80 | err := f.Write("foo") 81 | require.Nil(t, err) 82 | 83 | b, _ := os.ReadFile(filename) 84 | require.Equal(t, "foo", string(b)) 85 | } 86 | 87 | func TestWriteJSONFile(t *testing.T) { 88 | filename := "/tmp/go-test-text" 89 | defer os.Remove(filename) 90 | 91 | f := NewFile(filename) 92 | err := f.WriteJSON(&Foo{ID: 1, Name: "foo"}) 93 | require.Nil(t, err) 94 | 95 | b, _ := os.ReadFile(filename) 96 | require.Equal(t, "{\n \"ID\": 1,\n \"Name\": \"foo\"\n}", string(b)) 97 | 98 | } 99 | 100 | func TestWriteTemplateInFile(t *testing.T) { 101 | filename := "/tmp/go-test-tmpl" 102 | defer os.Remove(filename) 103 | 104 | foo := &Foo{ID: 1, Name: "foo"} 105 | f := NewFile(filename) 106 | _ = f.WriteTemplate("id: {{ .ID }}\nname: {{ .Name }}", foo) 107 | 108 | b, _ := os.ReadFile(filename) 109 | require.Equal(t, "id: 1\nname: foo", string(b)) 110 | } 111 | 112 | func TestWriteTemplateSliceInFile(t *testing.T) { 113 | filename := "/tmp/go-test-tmplslice" 114 | defer os.Remove(filename) 115 | 116 | foo := []interface{}{&Foo{ID: 1, Name: "foo"}, &Foo{ID: 2, Name: "bar"}} 117 | f := NewFile(filename) 118 | _ = f.WriteTemplateSlice("id: {{ .ID }}\nname: {{ .Name }}", foo) 119 | 120 | b, _ := os.ReadFile(filename) 121 | require.Equal(t, "id: 1\nname: fooid: 2\nname: bar", string(b)) 122 | } 123 | 124 | func TestBackupFile(t *testing.T) { 125 | filename := "/tmp/go-test-bkp" 126 | _ = os.WriteFile(filename, []byte("foo"), 0644) 127 | 128 | f := NewFile(filename) 129 | bkp, _ := f.Backup() 130 | 131 | require.True(t, strings.HasPrefix(bkp, filename)) 132 | 133 | b, _ := os.ReadFile(filename) 134 | require.Equal(t, "foo", string(b)) 135 | } 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leocomelli/asl 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/kr/pretty v0.2.0 // indirect 7 | github.com/mitchellh/go-homedir v1.1.0 8 | github.com/rs/zerolog v1.32.0 9 | github.com/spf13/cobra v1.8.0 10 | github.com/stretchr/testify v1.9.0 11 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 12 | gopkg.in/ini.v1 v1.67.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 7 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 8 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 9 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 10 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 15 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 16 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 17 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 18 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 19 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 20 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 21 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 25 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 26 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 27 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 28 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 29 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 30 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 31 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 34 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 35 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 36 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 38 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 39 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 40 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 41 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 44 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 47 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 49 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: asl 2 | env: 3 | - GO111MODULE=on 4 | - GOPROXY=https://proxy.golang.org 5 | before: 6 | hooks: 7 | - go mod download 8 | builds: 9 | - 10 | binary: asl 11 | id: asl 12 | ldflags: "-X main.Version={{.Version}} -X main.BuildDate={{.Date}} -X main.GitHash={{.Commit}}" 13 | env: 14 | - CGO_ENABLED=0 15 | flags: 16 | - -buildmode 17 | - exe 18 | goos: 19 | - darwin 20 | - linux 21 | goarch: 22 | - amd64 23 | - arm64 24 | goarm: 25 | - 7 26 | archives: 27 | - 28 | id: asl 29 | format: binary 30 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 31 | replacements: 32 | darwin: Darwin 33 | linux: Linux 34 | amd64: x86_64 35 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | logger "github.com/rs/zerolog/log" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // Options defines root command options 15 | type Options struct { 16 | Backup bool 17 | EKS bool 18 | ForceSSOLogin bool 19 | } 20 | 21 | var ( 22 | // Version contains the current version of the app. 23 | Version = "" 24 | // BuildDate contains the date and time of build process. 25 | BuildDate = "" 26 | // GitHash contains the hash of last commit in the repository. 27 | GitHash = "" 28 | 29 | opts = &Options{} 30 | ) 31 | 32 | const ( 33 | msgTmpl = `it worked! \o/ 34 | 35 | ***************************************************************************************************************** 36 | %s 37 | %s 38 | note that it will expire at %s 39 | after this time, you may safely rerun this cli to refresh your credentials 40 | ***************************************************************************************************************** 41 | ` 42 | ssoMsgTmpl = `SSO 43 | your new access key pair has been stored in the aws configuration file %s 44 | to use these credentials, set the AWS_PROFILE or call the aws cli with the --profile option. 45 | ` 46 | eksMsgTmpl = `EKS 47 | your kubernetes config has been updated in the kubeconfig file %s 48 | to use these contexts, run kubectl config set-context or call the kubectl with the --context option 49 | ` 50 | ) 51 | 52 | func main() { 53 | rootCmd := &cobra.Command{ 54 | Use: "asl", 55 | Short: "Get credentials for all accounts for which you have permission in AWS SSO", 56 | Long: ``, 57 | RunE: func(cmd *cobra.Command, args []string) error { 58 | cfg, err := LoadConfig(opts) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | sso := NewSSO(&SSOCli{}, cfg) 64 | err = sso.PersistConfig() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | ssoCred, err := sso.Login() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | accounts, err := sso.ListAccounts(ssoCred) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | c, err := sso.GetCredentials(ssoCred, accounts) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | res, err := sso.PersistCredentials(c) 85 | if err != nil { 86 | return err 87 | } 88 | ssoMsg := fmt.Sprintf(ssoMsgTmpl, res.Filename) 89 | 90 | var eksMsg string 91 | if opts.EKS { 92 | eks := NewEKS(&EKSCli{}, cfg) 93 | err := eks.UpdateKubeConfig(c) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | eksMsg = fmt.Sprintf(eksMsgTmpl, eks.KubeConfigPath) 99 | } 100 | 101 | logger.Info().Msgf(msgTmpl, ssoMsg, eksMsg, res.ExpiresAt) 102 | 103 | return nil 104 | }, 105 | } 106 | 107 | rootCmd.PersistentFlags().StringP("loglevel", "d", "info", "set log level [info|debug|trace]") 108 | rootCmd.PersistentFlags().BoolVarP(&opts.Backup, "backup", "b", false, "force a back up of the configuration files [.aws/config|.aws/credentials|.kube/config]") 109 | rootCmd.PersistentFlags().BoolVarP(&opts.EKS, "eks", "k", false, "configure kubectl so that you can connect to an Amazon EKS cluster") 110 | rootCmd.PersistentFlags().BoolVarP(&opts.ForceSSOLogin, "login", "l", false, "force login to review the SSO access token") 111 | 112 | logger.Logger = logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 113 | setLogLevel(os.Args) 114 | 115 | ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) 116 | defer cancel() 117 | 118 | rootCmd.AddCommand([]*cobra.Command{ 119 | configureCmd(ctx), 120 | versionCmd(ctx), 121 | }...) 122 | 123 | err := rootCmd.Execute() 124 | if err != nil { 125 | logger.Fatal().Err(err) 126 | } 127 | } 128 | 129 | func setLogLevel(args []string) { 130 | level := "info" 131 | for i, a := range args { 132 | if a == "--loglevel" { 133 | level = args[i+1] 134 | break 135 | } 136 | } 137 | 138 | logLevel, err := zerolog.ParseLevel(level) 139 | if err != nil { 140 | logLevel = zerolog.InfoLevel 141 | } 142 | zerolog.SetGlobalLevel(logLevel) 143 | } 144 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "regexp" 4 | 5 | var ( 6 | matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 7 | matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 8 | ) 9 | 10 | // Snake converts text to use slash as a separator 11 | func Snake(txt string) string { 12 | snake := matchFirstCap.ReplaceAllString(txt, "${1}-${2}") 13 | return matchAllCap.ReplaceAllString(snake, "${1}-${2}") 14 | } 15 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestEmptySnake(t *testing.T) { 10 | r := Snake("") 11 | require.Equal(t, "", r) 12 | } 13 | 14 | func TestRoleNameSnake(t *testing.T) { 15 | r := Snake("DevSSOLogin") 16 | require.Equal(t, "Dev-SSO-Login", r) 17 | } 18 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const tmpl = ` 11 | Version: %s 12 | BuildDate: %s 13 | GitCommit: %s 14 | ` 15 | 16 | func versionCmd(ctx context.Context) *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "version", 19 | Short: "Print the version number of ASL", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | fmt.Printf(tmpl, Version, BuildDate, GitHash) 22 | return nil 23 | }, 24 | } 25 | 26 | return cmd 27 | } 28 | --------------------------------------------------------------------------------