Skip to content

Commit

Permalink
Allow sorting of roles via an arbitrary field
Browse files Browse the repository at this point in the history
Add `--sort` and `--reverse` to the `list` command to enable
sorting by any available field and reverse sorting.  Sorting
always happens alphabetically and is case-sensitive.

AccountIds are also now always presented with leading zeros
as appropriate now.

Improve unit tests for AccountId's with a leading zero

Fixes #466
  • Loading branch information
synfinatic committed Jul 30, 2023
1 parent 69a81d0 commit 9fdbdf3
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 74 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Unreleased

## [v1.10.0] - 2023-07-29
## [v1.10.0] - 2023-07-30

### Bugs

Expand All @@ -14,12 +14,14 @@
* Authentication via your SSO provider no longer uses a Firefox container #486
* Bump to Go v1.19
* Bump to golangci-lint v1.52.2
* AccountId in the `list` command output are now presented with a leading zero

### New Features

* Profiles in ~/.aws/config now include the `region = XXX` option #481
* Add `FirstTag` support in the config for placing a tag at the top of the select list #445
* Support `eval` command in Windows PowerShell via Invoke-Expression #188
* Add support for `--sort` and `--reverse` flags for the `list` command #466

## [v1.9.10] - 2023-02-27

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PROJECT_VERSION := 1.9.10
PROJECT_VERSION := 1.10.0
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso

Expand Down
100 changes: 59 additions & 41 deletions cmd/aws-sso/list_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import (
)

type ListCmd struct {
ListFields bool `kong:"short='f',help='List available fields',xor='fields'"`
CSV bool `kong:"help='Generate CSV instead of a table',xor='fields'"`
ListFields bool `kong:"short='f',help='List available fields',xor='listfields'"`
CSV bool `kong:"help='Generate CSV instead of a table',xor='listfields'"`
Prefix string `kong:"short='P',help='Filter based on the <FieldName>=<Prefix>'"`
Fields []string `kong:"optional,arg,help='Fields to display',env='AWS_SSO_FIELDS',predictor='fieldList',xor='fields'"`
Fields []string `kong:"optional,arg,help='Fields to display',env='AWS_SSO_FIELDS',predictor='fieldList',xor='listfields'"`
Sort string `kong:"short='s',help='Sort results by the <FieldName>',default='AccountId',env='AWS_SSO_FIELD_SORT',predictor='fieldList'"`
Reverse bool `kong:"help='Reverse sort results',env='AWS_SSO_FIELD_SORT_REVERSE'"`
}

// what should this actually do?
Expand Down Expand Up @@ -81,7 +83,7 @@ func (cc *ListCmd) Run(ctx *RunContext) error {
fields = ctx.Cli.List.Fields
}

return printRoles(ctx, fields, ctx.Cli.List.CSV, prefixSearch)
return printRoles(ctx, fields, ctx.Cli.List.CSV, prefixSearch, ctx.Cli.List.Sort, ctx.Cli.List.Reverse)
}

// DefaultCmd has no args, and just prints the default fields and exists because
Expand All @@ -102,60 +104,76 @@ func (cc *DefaultCmd) Run(ctx *RunContext) error {
}
}

return printRoles(ctx, ctx.Settings.ListFields, false, []string{})
return printRoles(ctx, ctx.Settings.ListFields, false, []string{}, "AccountId", false)
}

// Print all our roles
func printRoles(ctx *RunContext, fields []string, csv bool, prefixSearch []string) error {
func printRoles(ctx *RunContext, fields []string, csv bool, prefixSearch []string, sortby string, reverse bool) error {
var err error
roles := ctx.Settings.Cache.GetSSO().Roles
tr := []gotable.TableStruct{}
idx := 0

// print in AccountId order
accounts := []int64{}
for account := range roles.Accounts {
accounts = append(accounts, account)
}
sort.Slice(accounts, func(i, j int) bool { return accounts[i] < accounts[j] })
allRoles := roles.GetAllRoles()

for _, account := range accounts {
// print roles in order
roleNames := []string{}
for _, roleFlat := range roles.GetAccountRoles(account) {
roleNames = append(roleNames, roleFlat.RoleName)
var sortError error
sort.SliceStable(allRoles, func(i, j int) bool {
a, err := allRoles[i].GetField(sortby)
if err != nil {
sortError = fmt.Errorf("Invalid --sort value: %s", sortby)
return false
}
sort.Strings(roleNames)

for _, roleName := range roleNames {
roleFlat, _ := roles.GetRole(account, roleName)
if !roleFlat.IsExpired() {
if exp, err := utils.TimeRemain(roleFlat.Expires, true); err == nil {
roleFlat.ExpiresStr = exp
}
b, _ := allRoles[j].GetField(sortby)

if a.Type == sso.Sval {
if !reverse {
return a.Sval < b.Sval
} else {
return a.Sval > b.Sval
}
// update Profile
p, err := roleFlat.ProfileName(ctx.Settings)
if err == nil {
roleFlat.Profile = p
} else if a.Type == sso.Ival {
if !reverse {
return a.Ival < b.Ival
} else {
return a.Ival > b.Ival
}
} else {
sortError = fmt.Errorf("Unable to sort by field: %s", sortby)
return false
}
})

if sortError != nil {
return sortError
}

for _, roleFlat := range allRoles {
if len(prefixSearch) > 0 {
match, err := roleFlat.HasPrefix(prefixSearch[0], prefixSearch[1])
if err != nil {
return err
}

if len(prefixSearch) > 0 {
match, err := roleFlat.HasPrefix(prefixSearch[0], prefixSearch[1])
if err != nil {
return err
}
if !match {
// skip because not a match
continue
}
}

if !match {
// skip because not a match
continue
}
if !roleFlat.IsExpired() {
if exp, err := utils.TimeRemain(roleFlat.Expires, true); err == nil {
roleFlat.ExpiresStr = exp
}
}

roleFlat.Id = idx
idx += 1
tr = append(tr, *roleFlat)
p, err := roleFlat.ProfileName(ctx.Settings)
if err == nil {
roleFlat.Profile = p
}

roleFlat.Id = idx
idx += 1
tr = append(tr, *roleFlat)
}

// Determine when our AWS SSO session expires
Expand Down
24 changes: 15 additions & 9 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,11 @@ which fields are printed by specifying the field names as arguments.
Flags:

* `--list-fields`, `-f` -- List the available fields to print
* `--prefix <Field>=<Prefix>`, `-P` -- Filter results by the given field
* `--prefix <FieldName>=<Prefix>`, `-P` -- Filter results by the given field
value & prefix value
* `--csv` -- Generate results in CSV format
* `--sort <FieldName>`, `-s` -- Sort results by the provided field name
* `--reverse` -- Reverse the sort order

Arguments: `[<field> ...]`

Expand All @@ -273,6 +275,8 @@ Default fields:
* `RoleName`
* `ExpiresStr`

**Note:** Sorting always happens in a case-sensitive and alphabetic manner.

---

### flush
Expand Down Expand Up @@ -354,14 +358,16 @@ the new settings if you've upgraded from a previous version!

The following environment variables are honored by `aws-sso`:

* `AWS_SSO_FILE_PASSWORD` -- Password to use with the `file` SecureStore
* `AWS_SSO_CONFIG` -- Specify an alternate path to the `aws-sso` config file
* `AWS_SSO_BROWSER` -- Override default browser for AWS SSO login
* `AWS_SSO` -- Override default AWS SSO instance to use
* `AWS_SSO_ROLE_NAME` -- Used for `--role`/`-R` with some commands
* `AWS_SSO_ACCOUNT_ID` -- Used for `--account`/`-A` with some commands
* `AWS_SSO_ROLE_ARN` -- Used for `--arn`/`-a` with some commands and with
`eval --refresh`
* `AWS_SSO_FILE_PASSWORD` -- Password to use with the `file` SecureStore.
* `AWS_SSO_CONFIG` -- Specify an alternate path to the `aws-sso` config file.
* `AWS_SSO_BROWSER` -- Override default browser for AWS SSO login.
* `AWS_SSO` -- Override default AWS SSO instance to use.
* `AWS_SSO_ROLE_NAME` -- Used for `--role`/`-R` with some commands.
* `AWS_SSO_ACCOUNT_ID` -- Used for `--account`/`-A` with some commands.
* `AWS_SSO_ROLE_ARN` -- Used for `--arn`/`-a` with some commands and with.
`eval --refresh`.
* `AWS_SSO_FIELD_SORT` -- Used by `list` command to select which field to sort by.
* `AWS_SSO_FIELD_SORT_REVERSE` -- Used to reverse the `list` sort order. Set to `1` to enable.

The `file` SecureStore will use the `AWS_SSO_FILE_PASSWORD` environment
variable for the password if it is set. (Not recommended.)
Expand Down
2 changes: 1 addition & 1 deletion sso/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func (suite *CacheTestSuite) TestGetAllRoles() {
assert.Equal(t, 4, len(aroles))
aroles = cache.Roles.GetAccountRoles(502470824893)
assert.Equal(t, 4, len(aroles))
aroles = cache.Roles.GetAccountRoles(258234615182)
aroles = cache.Roles.GetAccountRoles(25823461518)
assert.Equal(t, 7, len(aroles))
}

Expand Down
48 changes: 48 additions & 0 deletions sso/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,51 @@ func (r *AWSRoleFlat) HasPrefix(field, prefix string) (bool, error) {
}
return false, nil
}

type FlatFieldType int

const (
Serr FlatFieldType = iota
Sval
Ival
)

type FlatField struct {
Sval string
Ival int64
Type FlatFieldType
}

func (r *AWSRoleFlat) GetField(fieldName string) (FlatField, error) {
var err error
ret := FlatField{}
v := reflect.ValueOf(r)
f := reflect.Indirect(v).FieldByName(fieldName)

// Make sure the fieldName exists in our struct
if !f.IsValid() {
return ret, fmt.Errorf("Invalid field name: %s", fieldName)
}

switch fieldName {
case "AccountId":
ret.Type = Sval
ret.Sval, err = utils.AccountIdToString(int64(f.Int()))
if err != nil {
return ret, err
}

case "Expires":
ret.Type = Ival
ret.Ival = int64(f.Int())

case "Tags":
return ret, fmt.Errorf("Unable to sort by `Tags`")

default:
ret.Type = Sval
ret.Sval = f.String()
}

return ret, nil
}
41 changes: 35 additions & 6 deletions sso/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (suite *CacheRolesTestSuite) TestAccountIds() {
roles := suite.cache.SSO[suite.cache.ssoName].Roles

assert.NotEmpty(t, roles.AccountIds())
assert.Contains(t, roles.AccountIds(), int64(258234615182))
assert.Contains(t, roles.AccountIds(), int64(25823461518))
assert.NotContains(t, roles.AccountIds(), int64(2582346))
}

Expand All @@ -124,7 +124,7 @@ func (suite *CacheRolesTestSuite) TestGetAccountRoles() {
t := suite.T()
roles := suite.cache.SSO[suite.cache.ssoName].Roles

flat := roles.GetAccountRoles(258234615182)
flat := roles.GetAccountRoles(25823461518)
assert.NotEmpty(t, flat)

flat = roles.GetAccountRoles(258234615)
Expand All @@ -147,7 +147,7 @@ func (suite *CacheRolesTestSuite) TestGetRoleTags() {

tags := *(roles.GetRoleTags())
assert.NotEmpty(t, tags)
arn := "arn:aws:iam::258234615182:role/AWSAdministratorAccess"
arn := "arn:aws:iam::025823461518:role/AWSAdministratorAccess"
assert.Contains(t, tags, arn)
assert.NotContains(t, tags, "foobar")
assert.Contains(t, tags[arn]["Email"], "control-tower-dev-aws@ourcompany.com")
Expand All @@ -161,9 +161,9 @@ func (suite *CacheRolesTestSuite) TestGetRole() {
_, err := roles.GetRole(58234615182, "AWSAdministratorAccess")
assert.Error(t, err)

r, err := roles.GetRole(258234615182, "AWSAdministratorAccess")
r, err := roles.GetRole(25823461518, "AWSAdministratorAccess")
assert.NoError(t, err)
assert.Equal(t, int64(258234615182), r.AccountId)
assert.Equal(t, int64(25823461518), r.AccountId)
assert.Equal(t, "AWSAdministratorAccess", r.RoleName)
assert.Equal(t, "", r.Profile)
assert.Equal(t, "us-east-1", r.DefaultRegion)
Expand All @@ -176,7 +176,7 @@ func (suite *CacheRolesTestSuite) TestGetRole() {
func (suite *CacheRolesTestSuite) TestProfileName() {
t := suite.T()
roles := suite.cache.SSO[suite.cache.ssoName].Roles
r, err := roles.GetRole(258234615182, "AWSAdministratorAccess")
r, err := roles.GetRole(25823461518, "AWSAdministratorAccess")
assert.NoError(t, err)

p, err := r.ProfileName(suite.settings)
Expand Down Expand Up @@ -233,6 +233,35 @@ func (suite *CacheRolesTestSuite) TestGetEnvVarTags() {
assert.Equal(t, x, flat.GetEnvVarTags(&settings))
}

func TestGetField(t *testing.T) {
flat := AWSRoleFlat{
RoleName: "foobar",
AccountId: 12344553243,
Expires: 0,
}

f, err := flat.GetField("RoleName")
assert.NoError(t, err)
assert.Equal(t, Sval, f.Type)
assert.Equal(t, "foobar", f.Sval)

f, err = flat.GetField("AccountId")
assert.NoError(t, err)
assert.Equal(t, Sval, f.Type)
assert.Equal(t, "012344553243", f.Sval)

f, err = flat.GetField("Expires")
assert.NoError(t, err)
assert.Equal(t, Ival, f.Type)
assert.Equal(t, int64(0), f.Ival)

f, err = flat.GetField("Tags")
assert.Error(t, err)

f, err = flat.GetField("Role")
assert.Error(t, err)
}

func TestAWSRoleFlatGetHeader(t *testing.T) {
f := AWSRoleFlat{}
x, err := f.GetHeader("ExpiresStr")
Expand Down
Loading

0 comments on commit 9fdbdf3

Please sign in to comment.