Skip to content

Commit

Permalink
Add support for credentials command
Browse files Browse the repository at this point in the history
Write our credentials in the format of ~/.aws/credentials for tooling
that requires this.

Fixes: #867
  • Loading branch information
synfinatic committed Jun 4, 2024
1 parent 4828e6e commit 3f2b9e6
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 7 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

## [Unreleased]

## [v1.16.0] - XXXX-XX-XX

### New Features

* Add credentials command #867

## [v1.15.1] - 2024-04-30

### New Features
### New Features

* Add helper aliases for fish shell #361

Expand Down Expand Up @@ -660,7 +666,8 @@

Initial release

[Unreleased]: https://github.com/synfinatic/aws-sso-cli/compare/v1.15.1...main
[Unreleased]: https://github.com/synfinatic/aws-sso-cli/compare/v1.16.0...main
[v1.16.0]: https://github.com/synfinatic/aws-sso-cli/releases/tag/v1.16.0
[v1.15.1]: https://github.com/synfinatic/aws-sso-cli/releases/tag/v1.15.1
[v1.15.0]: https://github.com/synfinatic/aws-sso-cli/releases/tag/v1.15.0
[v1.14.3]: https://github.com/synfinatic/aws-sso-cli/releases/tag/v1.14.3
Expand Down
4 changes: 2 additions & 2 deletions cmd/aws-sso/cache_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ func (cc *CacheCmd) Run(ctx *RunContext) error {

err = ctx.Settings.Cache.Refresh(awssso, s, ssoName)
if err != nil {
return fmt.Errorf("Unable to refresh role cache: %s", err.Error())
return fmt.Errorf("unable to refresh role cache: %s", err.Error())
}
ctx.Settings.Cache.PruneSSO(ctx.Settings)

err = ctx.Settings.Cache.Save(true)
if err != nil {
return fmt.Errorf("Unable to save role cache: %s", err.Error())
return fmt.Errorf("unable to save role cache: %s", err.Error())
}

return nil
Expand Down
54 changes: 54 additions & 0 deletions cmd/aws-sso/credentials_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"os"

"github.com/synfinatic/aws-sso-cli/internal/awsconfig"
)

type CredentialsCmd struct {
File string `kong:"short='f',help='File to write credentials to (default: stdout)',predictor='allFiles'"`
Append bool `kong:"short='a',help='Append to the file instead of overwriting'"`
Profiles []string `kong:"required,short='p',name='profiles',help='Profiles to write credentials for',predictor='profile'"`
}

func (cc *CredentialsCmd) Run(ctx *RunContext) error {
cache := ctx.Settings.Cache.GetSSO()
awssso := doAuth(ctx)

creds := []awsconfig.ProfileCredentials{}

for _, profile := range ctx.Cli.Credentials.Profiles {
roleFlat, err := cache.Roles.GetRoleByProfile(profile, ctx.Settings)
if err != nil {
return err
}

pCreds := GetRoleCredentials(ctx, awssso, roleFlat.AccountId, roleFlat.RoleName)

creds = append(creds, awsconfig.ProfileCredentials{
Profile: profile,
AccessKeyId: pCreds.AccessKeyId,
SecretAccessKey: pCreds.SecretAccessKey,
SessionToken: pCreds.SessionToken,
Expires: pCreds.ExpireString(),
})
}

var err error
switch cc.File {
case "":
err = awsconfig.PrintProfileCredentials(creds)

default:
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if cc.Append {
flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
}
err = awsconfig.WriteProfileCredentials(ctx.Cli.Credentials.File, flags, creds)

Check failure on line 49 in cmd/aws-sso/credentials_cmd.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary trailing newline (whitespace)
}

return err

Check failure on line 53 in cmd/aws-sso/credentials_cmd.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary trailing newline (whitespace)
}
6 changes: 3 additions & 3 deletions cmd/aws-sso/eval_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (cc *EvalCmd) Run(ctx *RunContext) error {
// refreshing?
if ctx.Cli.Eval.Refresh {
if ctx.Cli.Eval.EnvArn == "" {
return fmt.Errorf("Unable to determine current IAM role")
return fmt.Errorf("%s", "Unable to determine current IAM role")
}
accountid, role, err = utils.ParseRoleARN(ctx.Cli.Eval.EnvArn)
if err != nil {
Expand All @@ -78,7 +78,7 @@ func (cc *EvalCmd) Run(ctx *RunContext) error {
role = ctx.Cli.Eval.Role
accountid = ctx.Cli.Eval.AccountId
} else {
return fmt.Errorf("Please specify --refresh, --clear, --arn, or --account and --role")
return fmt.Errorf("%s", "Please specify --refresh, --clear, --arn, or --account and --role")
}
region := ctx.Settings.GetDefaultRegion(accountid, role, ctx.Cli.Eval.NoRegion)

Expand All @@ -95,7 +95,7 @@ func (cc *EvalCmd) Run(ctx *RunContext) error {
// powershell Invoke-Expression https://github.com/synfinatic/aws-sso-cli/issues/188
fmt.Printf("$Env:%s = \"%s\"\r\n", k, v)
} else {
return fmt.Errorf("invalid or unsupported shell. Please file a bug!")
return fmt.Errorf("%s", "invalid or unsupported shell. Please file a bug!")
}
}
return nil
Expand Down
3 changes: 3 additions & 0 deletions cmd/aws-sso/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/alecthomas/kong"
"github.com/posener/complete"

// "github.com/davecgh/go-spew/spew"
"github.com/sirupsen/logrus"
"github.com/synfinatic/aws-sso-cli/internal/awscreds"
Expand Down Expand Up @@ -118,6 +119,7 @@ type CLI struct {
// Commands
Cache CacheCmd `kong:"cmd,help='Force reload of cached AWS SSO role info and config.yaml'"`
Console ConsoleCmd `kong:"cmd,help='Open AWS Console using specificed AWS role/profile'"`
Credentials CredentialsCmd `kong:"cmd,help='Generate static AWS credentials for use with AWS CLI'"`
Default DefaultCmd `kong:"cmd,hidden,default='1'"` // list command without args
Eval EvalCmd `kong:"cmd,help='Print AWS environment vars for use with eval $(aws-sso eval ...)'"`
Exec ExecCmd `kong:"cmd,help='Execute command using specified IAM role in a new shell'"`
Expand Down Expand Up @@ -250,6 +252,7 @@ func parseArgs(cli *CLI) (*kong.Context, sso.OverrideSettings) {
"region": p.RegionComplete(),
"role": p.RoleComplete(),
"sso": p.SsoComplete(),
"allFiles": complete.PredictFiles("*"),
},
),
)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ require (
)

require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/aws/aws-sdk-go-v2/config v1.19.1
github.com/aws/aws-sdk-go-v2/credentials v1.13.43
github.com/aws/aws-sdk-go-v2/service/iam v1.24.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
Expand Down
74 changes: 74 additions & 0 deletions internal/awsconfig/credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2024 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"fmt"
"io"
"os"
"text/template"
)

const (
CREDENTIALS_TEMPLATE = `{{range $profile := . }}
[{{ $profile.Profile }}]
# Expires: {{ $profile.Expires }}
aws_access_key_id = {{ $profile.AccessKeyId }}
aws_secret_access_key = {{ $profile.SecretAccessKey }}
aws_session_token = {{ $profile.SessionToken }}
{{end}}
`
)

type ProfileCredentials struct {
Profile string
AccessKeyId string
SecretAccessKey string
SessionToken string
Expires string
}

func genProfileCredentials(output io.Writer, creds []ProfileCredentials) error {
if len(creds) == 0 {
return fmt.Errorf("no credentials to write")
}
t := template.Must(template.New("template").Parse(CREDENTIALS_TEMPLATE))
return t.Execute(output, creds)
}

// AwsCredentialsFile generates a new AWS credentials file or writes to STDOUT
// cfile is the path to the file to write to, or "" to write to stdout
// flags is the flags to pass to os.OpenFile
// creds is the list of credentials to write
func WriteProfileCredentials(cfile string, flags int, creds []ProfileCredentials) error {
var ofile *os.File
var err error

ofile, err = os.OpenFile(cfile, flags, 0600)
if err != nil {
return err
}
defer ofile.Close()

return genProfileCredentials(ofile, creds)
}

func PrintProfileCredentials(creds []ProfileCredentials) error {
return genProfileCredentials(os.Stdout, creds)
}
95 changes: 95 additions & 0 deletions internal/awsconfig/credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package awsconfig

/*
* AWS SSO CLI
* Copyright (c) 2021-2024 Aaron Turner <synfinatic at gmail dot com>
*
* This program is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or with the authors permission any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import (
"bytes"
"io"
"os"
"testing"

"github.com/MakeNowJust/heredoc"

"github.com/stretchr/testify/assert"
)

func TestGenProfileCredentials(t *testing.T) {
// Create a buffer to capture STDOUT
buf := &bytes.Buffer{}

// Create example ProfileCredentials
creds := []ProfileCredentials{
{
Profile: "first",
AccessKeyId: "AKIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
SessionToken: "AQoDYXdzEJr...<remainder of security token>",
Expires: "2024-06-03 17:56:11 -0700 PDT",
},
{
Profile: "second",
AccessKeyId: "AKIAYOMAMMAEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/YESMAN/bPxRfiCYEXAMPLEKEY",
SessionToken: "AQoEdBaglyJunior...<remainder of security token>",
Expires: "2024-06-03 18:58:01 -0700 PDT",
},
}

err := genProfileCredentials(buf, creds)
assert.NoError(t, err)

credsResult := heredoc.Doc(`
[first]
# Expires: 2024-06-03 17:56:11 -0700 PDT
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
aws_session_token = AQoDYXdzEJr...<remainder of security token>
[second]
# Expires: 2024-06-03 18:58:01 -0700 PDT
aws_access_key_id = AKIAYOMAMMAEXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/YESMAN/bPxRfiCYEXAMPLEKEY
aws_session_token = AQoEdBaglyJunior...<remainder of security token>
`)

assert.Equal(t, credsResult, buf.String())

// replace os.Stdout with our buffer
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err = PrintProfileCredentials(creds)
assert.NoError(t, err)
w.Close()
output, _ := io.ReadAll(r)
assert.Equal(t, credsResult, string(output))

// restore stdout
os.Stdout = old
}

func TestGenProfileCredentialsErrors(t *testing.T) {
// Test with an empty slice
buf := &bytes.Buffer{}
err := genProfileCredentials(buf, []ProfileCredentials{})
assert.Error(t, err)
}

0 comments on commit 3f2b9e6

Please sign in to comment.