Skip to content

Commit

Permalink
Add config support and Profile to Roles
Browse files Browse the repository at this point in the history
* Roles now support specifying a custom `Profile` value
    which is used as `AWS_SSO_PROFILE` and for the profile name
    in ~/.aws/config
* Add support for the `config` command which generates the necessary
    profile entries in ~/.aws/config
* Add StringReplace function for ProfileFormat

Refs: #157, #212
  • Loading branch information
synfinatic committed Jan 3, 2022
1 parent 3ce43f8 commit 8688c39
Show file tree
Hide file tree
Showing 15 changed files with 791 additions and 378 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
PROJECT_VERSION := 1.6.1
PROJECT_VERSION := 1.7.0
DOCKER_REPO := synfinatic
PROJECT_NAME := aws-sso

Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
* [Quick Setup](#quick-setup)
* [Security](#security)
* [Commands](#commands)
* [cache](#cache)
* [console](#console)
* [config](#config)
* [eval](#eval)
* [exec](#exec)
* [flush](#flush)
* [list](#list)
* [process](#process)
* [tags](#tags)
* [time](#time)
* [install-autocomplete](#install-autocomplete)
* [Configuration](docs/config.md)
* [Environment Varables](#environment-varables)
* [Release History](#release-history)
Expand Down Expand Up @@ -130,6 +141,7 @@ been granted access!

* [cache](#cache) -- Force refresh of AWS SSO role information
* [console](#console) -- Open AWS Console in a browser with the selected role
* [config](#config) -- Update your `~/.aws/config` file with the AWS profiles in AWS SSO
* [eval](#eval) -- Print shell environment variables for use in your shell
* [exec](#exec) -- Exec a command with the selected role
* [flush](#flush) -- Force delete of cached AWS SSO credentials
Expand Down Expand Up @@ -179,6 +191,33 @@ Priority is given to:
* `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables
* Prompt user interactively

### config

Modifies the `~/.aws/config` file to contain a profile for every role accessible
via AWS SSO CLI.

Flags:

* `--print` -- Print profile entries instead of modifying config file
* `--output` -- Set the default output format.
Must be one of `json`, `yaml`, `yaml-stream`, `text`, `table`. Default is `json`.

This generates a series of [named profile entries](
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) in the
`~/.aws/config` file which allows you to easily use any AWS SSO role just by setting
the `$AWS_PROFILE` environment variable. By default, each profile is named according
to the [ProfileFormat](docs/config.md#profileformat) config option or overridden by
the user defined [Profile](docs/config.md#profile) option on a role by role basis.

**Note:** You should run this command any time your list of AWS roles changes.

**Note:** It is important that you do _NOT_ remove the `# BEGIN_AWS_SSO_CLI` and
`# END_AWS_SSO_CLI` lines from your config file! These markers are used to track
which profiles are managed by AWS SSO CLI.

**Note:** This command does not honor the `--sso` option as it operates on all
of the configured AWS SSO instances in the `~/.aws-sso/config.yaml` file.

### eval

Generate a series of `export VARIABLE=VALUE` lines suitable for sourcing into your
Expand Down Expand Up @@ -319,12 +358,21 @@ By default the following key/values are available as tags to your roles:
* `History` -- Tag tracking if this role was recently used. See `HistoryLimit`
in config.

### time

Print a string containing the number of hours and minutes that the current
AWS Role's STS credentials are valid for in the format of `HHhMMm`

### install-autocomplete

Configures your appropriate shell configuration file to add auto-complete
functionality for commands, flags and options. Must restart your shell
for this to take effect.

Modifies the following file based on your shell:
* `~/.bash_profile` -- bash
* `~/.zshrc` -- zsh

## Environment Varables

### Honored Variables
Expand Down
91 changes: 91 additions & 0 deletions cmd/config_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

/*
* AWS SSO CLI
* Copyright (c) 2021-2022 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"
"os"
"text/template"

log "github.com/sirupsen/logrus"
)

const (
// CONFIG_PREFIX = "# BEGIN_AWS_SSO_CLI"
// CONFIG_SUFFIX = "# END_AWS_SSO_CLI"
CONFIG_TEMPLATE = `
# BEGIN_AWS_SSO_CLI
{{ range . }}
[profile {{ .Profile }}]
credential_process = {{ .BinaryPath }} -u open -S "{{ .Sso }}" process --arn {{ .Arn }}
output={{ .Output }}
{{end}}
# END_AWS_SSO_CLI
`
)

type ProfileConfig struct {
Sso string
Arn string
Profile string
Output string
BinaryPath string
}

type ConfigCmd struct {
Print bool `kong:"help='Print profile entries instead of modifying config file'"`
Output string `kong:"help='Output format [json|yaml|yaml-stream|text|table]',default='json',enum='json,yaml,yaml-stream,text,table'"`
}

func (cc *ConfigCmd) Run(ctx *RunContext) error {
set := ctx.Settings
binaryPath, _ := os.Executable()

// Find all the roles across all of the SSO instances
profiles := []ProfileConfig{}
for ssoName, s := range set.Cache.SSO {
for _, role := range s.Roles.GetAllRoles() {
profile, err := role.ProfileName(ctx.Settings)
if err != nil {
log.Errorf("Unable to generate profile name for %s: %s", role.Arn, err.Error())
}
profiles = append(profiles, ProfileConfig{
Sso: ssoName,
Arn: role.Arn,
Profile: profile,
Output: ctx.Cli.Config.Output,
BinaryPath: binaryPath,
})
}
}

templ, err := template.New("profile").Parse(CONFIG_TEMPLATE)
if err != nil {
return err
}
if ctx.Cli.Config.Print {
if err := templ.Execute(os.Stdout, profiles); err != nil {
return err
}
} else {
return fmt.Errorf("Writing to ~/.aws/config is not yet supported")
}

return nil
}
61 changes: 5 additions & 56 deletions cmd/exec_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@ package main
*/

import (
"bytes"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"text/template"

"github.com/c-bata/go-prompt"
"github.com/davecgh/go-spew/spew"
log "github.com/sirupsen/logrus"
"github.com/synfinatic/aws-sso-cli/sso"
"github.com/synfinatic/aws-sso-cli/utils"
Expand Down Expand Up @@ -105,32 +101,6 @@ func (cc *ExecCmd) Run(ctx *RunContext) error {
return nil
}

const (
AwsSsoProfileTemplate = "{{AccountIdStr .AccountId}}:{{.RoleName}}"
)

func emptyString(str string) bool {
return str == ""
}

func firstItem(items ...string) string {
for _, v := range items {
if v != "" {
return v
}
}
return ""
}

func accountIdToStr(id int64) string {
i, _ := utils.AccountIdToString(id)
return i
}

func stringsJoin(x string, items ...string) string {
return strings.Join(items, x)
}

// Executes Cmd+Args in the context of the AWS Role creds
func execCmd(ctx *RunContext, awssso *sso.AWSSSO, accountid int64, role string) error {
region := ctx.Settings.GetDefaultRegion(ctx.Cli.Exec.AccountId, ctx.Cli.Exec.Role, ctx.Cli.Exec.NoRegion)
Expand Down Expand Up @@ -158,6 +128,7 @@ func execCmd(ctx *RunContext, awssso *sso.AWSSSO, accountid int64, role string)
}

func execShellEnvs(ctx *RunContext, awssso *sso.AWSSSO, accountid int64, role, region string) map[string]string {
var err error
credsPtr := GetRoleCredentials(ctx, awssso, accountid, role)
creds := *credsPtr

Expand All @@ -180,39 +151,17 @@ func execShellEnvs(ctx *RunContext, awssso *sso.AWSSSO, accountid int64, role, r
shellVars["AWS_SSO_DEFAULT_REGION"] = ""
}

var profileFormat string = AwsSsoProfileTemplate

funcMap := template.FuncMap{
"AccountIdStr": accountIdToStr,
"EmptyString": emptyString,
"FirstItem": firstItem,
"StringsJoin": stringsJoin,
}

if ctx.Settings.ProfileFormat != "" {
profileFormat = ctx.Settings.ProfileFormat
}

// Set the AWS_SSO_PROFILE env var using our template
var templ *template.Template
cache := ctx.Settings.Cache.GetSSO()
if roleInfo, err := cache.Roles.GetRole(accountid, role); err != nil {
var roleInfo *sso.AWSRoleFlat
if roleInfo, err = cache.Roles.GetRole(accountid, role); err != nil {
// this error should never happen
log.Errorf("Unable to find role in cache. Unable to set AWS_SSO_PROFILE")
} else {
templ, err = template.New("main").Funcs(funcMap).Parse(profileFormat)
shellVars["AWS_SSO_PROFILE"], err = roleInfo.ProfileName(ctx.Settings)
if err != nil {
log.Errorf("Invalid ProfileFormat '%s': %s -- using default", ctx.Settings.ProfileFormat, err)
templ, _ = template.New("main").Funcs(funcMap).Parse(AwsSsoProfileTemplate)
}

buf := new(bytes.Buffer)
log.Tracef("RoleInfo: %s", spew.Sdump(roleInfo))
log.Tracef("Template: %s", spew.Sdump(templ))
if err := templ.Execute(buf, roleInfo); err != nil {
log.WithError(err).Errorf("Unable to generate AWS_SSO_PROFILE")
log.Errorf("Unable to generate AWS_SSO_PROFILE: %s", err.Error())
}
shellVars["AWS_SSO_PROFILE"] = buf.String()
}

return shellVars
Expand Down
7 changes: 6 additions & 1 deletion cmd/list_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var allListFields = map[string]string{
"RoleName": "AWS Role Name",
"SSO": "AWS SSO Instance Name",
"Via": "Role Chain Via",
// "Profile": "AWS_PROFILE",
"Profile": "AWS_SSO_PROFILE / AWS_PROFILE",
}

type ListCmd struct {
Expand Down Expand Up @@ -118,6 +118,11 @@ func printRoles(ctx *RunContext, fields []string) {
roleFlat.ExpiresStr = exp
}
}
// update Profile
p, err := roleFlat.ProfileName(ctx.Settings)
if err == nil {
roleFlat.Profile = p
}
roleFlat.Id = idx
idx += 1
tr = append(tr, *roleFlat)
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type CLI struct {

// Commands
Cache CacheCmd `kong:"cmd,help='Force reload of cached AWS SSO role info and config.yaml'"`
Config ConfigCmd `kong:"cmd,help='Update ~/.aws/config with AWS SSO profiles'"`
Console ConsoleCmd `kong:"cmd,help='Open AWS Console using specificed AWS Role/profile'"`
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 ...)'"`
Expand Down
12 changes: 6 additions & 6 deletions cmd/process_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ package main
import (
"encoding/json"
"fmt"
"time"

//log "github.com/sirupsen/logrus"
// log "github.com/sirupsen/logrus"
"github.com/synfinatic/aws-sso-cli/sso"
"github.com/synfinatic/aws-sso-cli/storage"
"github.com/synfinatic/aws-sso-cli/utils"
Expand Down Expand Up @@ -71,12 +70,13 @@ type CredentialProcessOutput struct {
}

func NewCredentialsProcessOutput(creds *storage.RoleCredentials) *CredentialProcessOutput {
x := *creds
c := CredentialProcessOutput{
Version: 1,
AccessKeyId: (*creds).AccessKeyId,
SecretAccessKey: (*creds).SecretAccessKey,
SessionToken: (*creds).SessionToken,
Expiration: time.Unix((*creds).ExpireEpoch(), 0).Format(time.RFC3339),
AccessKeyId: x.AccessKeyId,
SecretAccessKey: x.SecretAccessKey,
SessionToken: x.SessionToken,
Expiration: x.ExpireISO8601(),
}
return &c
}
Expand Down
36 changes: 26 additions & 10 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ If the above are true, then AWS SSO will define both:
* `$AWS_DEFAULT_REGION`
* `$AWS_SSO_DEFAULT_REGION`

to the default region as defined by `config.yaml`. If the user changes
to the default region as def)ined by `config.yaml`. If the user changes
roles and the two variables are set to the same region, then AWS SSO will
update the region. If the user ever overrides the `$AWS_DEFAULT_REGION`
value or deletes the `$AWS_SSO_DEFAULT_REGION` then AWS SSO will no longer
Expand All @@ -65,18 +65,34 @@ manage the variable.

### How to configure ProfileFormat

`aws-sso` makes it easy to modify your shell `$PROMPT` to include information
about what AWS Account/Role you have currently assumed by defining the `$AWS_SSO_PROFILE`
environment variable. By default, `ProfileFormat` is set to
`{{ AccountIdStr .AccountId }}:{{ .RoleName }}` which will generate a value like
`02345678901:MyRoleName`.
`aws-sso` uses the `ProfileFormat` configuration option for two different purposes:

1. Makes it easy to modify your shell `$PROMPT` to include information
about what AWS Account/Role you have currently assumed by defining the
`$AWS_SSO_PROFILE` environment variable.
2. Makes it easy to select a role via the `$AWS_PROFILE` environment variable
when you use the [config](../README.md#config) command.

By default, `ProfileFormat` is set to `{{ AccountIdStr .AccountId }}:{{ .RoleName }}`
which will generate a value like `02345678901:MyRoleName`.

Some examples:

* `{{ FirstItem .AccountName .AccountAlias }}` -- If there is an Account Name set in the config.yaml use that,
otherwise use the Account Alias defined by the AWS administrator.
* `{{ AccountIdStr .AccountId }}` -- Pad the AccountId with leading zeros if it is < 12 digits long
* `{{ FirstItem .AccountName .AccountAlias }}` -- If there is an Account Name
set in the config.yaml print that, otherwise print the Account Alias defined
by the AWS administrator.
* `{{ AccountIdStr .AccountId }}` -- Pad the AccountId with leading zeros if it
is < 12 digits long
* `{{ .AccountId }}` -- Print the AccountId as a regular number
* `{{ StringsJoin ":" .AccountAlias .RoleName}} -- Another way of writing `{{ .AccountAlias }}:{{ .RoleName }}`
* `{{ StringsJoin ":" .AccountAlias .RoleName }} -- Another way of writing
`{{ .AccountAlias }}:{{ .RoleName }}`
* `{{ StringReplace " " "_" .AccountAlias }}` -- Replace any spaces (` `) in the
AccountAlias with an underscore (`_`).
* `{{ FirstItem .AccountName .AccountAlias | StringReplace " " "_" }}:{{ .RoleName }}` --
Use the Account Name if set, otherwise use the Account Alias and replace any spaces
with an underscore and then append a colon, followed by the role name.

For a full list of available variables, [see here](config.md#profileformat).

To see a list of values across your roles for a given variable, you can use
the [list](../README.md#list) command.
Loading

0 comments on commit 8688c39

Please sign in to comment.