Skip to content

Commit

Permalink
Multiple okta-aws-cli configurations in okta.yaml by AWS profile name.
Browse files Browse the repository at this point in the history
Closes #162
Closes #36
  • Loading branch information
monde committed Feb 13, 2024
1 parent 2992d2b commit 91305d7
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 33 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ TBD

### ENHANCEMENTS

* Multiple okta-aws-cli configurations in `okta.yaml` by AWS profile name.
[#162](https://github.com/okta/okta-aws-cli/pull/162), thanks [@MatthewJohn](https://github.com/MatthewJohn)!

* Explicitly set AWS Region with CLI flag `--aws-region` [#174](https://github.com/okta/okta-aws-cli/pull/174), thanks [@euchen-circle](https://github.com/euchen-circle), [@igaskin](https://github.com/igaskin)!

### BUG FIXES
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ format.
- [Web command settings](#web-command-settings)
- [M2M command settings](#m2m-command-settings)
- [Friendly IdP and Role menu labels](#friendly-idp-and-role-menu-labels)
- [Configuration by profile name](#configuration-by-profile-name)
- [Debug okta.yaml](#debug-oktayaml)
- [Installation](#installation)
- [Recommendations](#recommendations)
- [Operation](#operation)
Expand Down Expand Up @@ -515,7 +517,39 @@ awscli:
Ops
```

#### Debug okta.yaml
### Configuration by profile name

Multiple `okta-aws-cli` configurations can be saved in the `$HOME/.okta/okta.yaml`
file and are keyed by AWS profile name in the `awscli.profiles` section. This
allows the operator to save many `okta-aws-cli` configurations in the okta.yaml.

```
$ okta-aws-cli web --profile staging
```

#### Example `$HOME/.okta/okta.yaml`

```yaml
---
awscli:
profiles:
staging:
oidc-client-id: "0osabc"
org-domain: "org-stg.okata.com"
aws-iam-idp: "arn:aws:iam::123:saml-provider/MyIdP"
aws-iam-role: "arn:aws:iam::123:role/S3_Read"
write-aws-credentials: true
open-browser: true
production:
oidc-client-id: "0opabc"
org-domain: "org-prd.okata.com"
aws-iam-idp: "arn:aws:iam::456:saml-provider/MyIdP"
aws-iam-role: "arn:aws:iam::456:role/S3_Read"
write-aws-credentials: true
open-browser: true
```
## Debug okta.yaml
okta-aws-cli has a debug option to check if the okta.yaml file is readable and
in valid format.
Expand Down
119 changes: 113 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package config

import (
"bytes"
"fmt"
"net/http"
"net/url"
Expand All @@ -41,6 +42,10 @@ const (
// Version app version
Version = "2.0.1"

////////////////////////////////////////////////////////////
// FORMATS
////////////////////////////////////////////////////////////

// AWSCredentialsFormat format const
AWSCredentialsFormat = "aws-credentials"
// EnvVarFormat format const
Expand All @@ -50,6 +55,12 @@ const (
// NoopFormat format const
NoopFormat = "noop"

////////////////////////////////////////////////////////////
// FLAGS
// NOTE: if a new Flag value is added be sure to update the
// OktaYamlConfigProfile struct with that new value.
////////////////////////////////////////////////////////////

// AllProfilesFlag cli flag const
AllProfilesFlag = "all-profiles"
// AuthzIDFlag cli flag const
Expand Down Expand Up @@ -103,6 +114,10 @@ const (
// CacheAccessTokenFlag cli flag const
CacheAccessTokenFlag = "cache-access-token"

////////////////////////////////////////////////////////////
// ENV VARS
////////////////////////////////////////////////////////////

// AllProfilesEnvVar env var const
AllProfilesEnvVar = "OKTA_AWSCLI_ALL_PROFILES"
// AuthzIDEnvVar env var const
Expand Down Expand Up @@ -162,6 +177,10 @@ const (
// WriteAWSCredentialsEnvVar env var const
WriteAWSCredentialsEnvVar = "OKTA_AWSCLI_WRITE_AWS_CREDENTIALS"

////////////////////////////////////////////////////////////
// Other
////////////////////////////////////////////////////////////

// CannotBeBlankErrMsg error message const
CannotBeBlankErrMsg = "cannot be blank"
// OrgDomainMsg error message const
Expand All @@ -176,11 +195,42 @@ const (
// OktaYamlConfig represents config settings from $HOME/.okta/okta.yaml
type OktaYamlConfig struct {
AWSCLI struct {
IDPS map[string]string `yaml:"idps"`
ROLES map[string]string `yaml:"roles"`
IDPS map[string]string `yaml:"idps"`
ROLES map[string]string `yaml:"roles"`
PROFILES map[string]OktaYamlConfigProfile `yaml:"profiles"`
} `yaml:"awscli"`
}

// OktaYamlConfigProfile represents config settings that are indexed by profile name
type OktaYamlConfigProfile struct {
AllProfiles string `yaml:"all-profiles"`
AuthzID string `yaml:"authz-id"`
AWSAcctFedAppID string `yaml:"aws-acct-fed-app-id"`
AWSCredentials string `yaml:"aws-credentials"`
AWSIAMIdP string `yaml:"aws-iam-idp"`
AWSIAMRole string `yaml:"aws-iam-role"`
AWSRegion string `yaml:"aws-region"`
CustomScope string `yaml:"custom-scope"`
Debug string `yaml:"debug"`
DebugAPICalls string `yaml:"debug-api-calls"`
Exec string `yaml:"exec"`
Format string `yaml:"format"`
OIDCClientID string `yaml:"oidc-client-id"`
OpenBrowser string `yaml:"open-browser"`
OpenBrowserCommand string `yaml:"open-browser-command"`
OrgDomain string `yaml:"org-domain"`
PrivateKey string `yaml:"private-key"`
PrivateKeyFile string `yaml:"private-key-file"`
KeyID string `yaml:"key-id"`
Profile string `yaml:"profile"`
QRCode string `yaml:"qr-code"`
SessionDuration string `yaml:"session-duration"`
WriteAWSCredentials string `yaml:"write-aws-credentials"`
LegacyAWSVariables string `yaml:"legacy-aws-variables"`
ExpiryAWSVariables string `yaml:"expiry-aws-variables"`
CacheAccessToken string `yaml:"cache-access-token"`
}

// Clock interface to abstract time operations
type Clock interface {
Now() time.Time
Expand Down Expand Up @@ -318,12 +368,36 @@ func NewConfig(attrs *Attributes) (*Config, error) {
func getFlagNameFromProfile(awsProfile string, flag string) string {
profileKey := fmt.Sprintf("%s.%s", awsProfile, flag)
if awsProfile != "" && viper.IsSet(profileKey) == true {

Check failure on line 370 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / build

should omit comparison to bool constant, can be simplified to viper.IsSet(profileKey) (S1002)

Check failure on line 370 in internal/config/config.go

View workflow job for this annotation

GitHub Actions / build

should omit comparison to bool constant, can be simplified to viper.IsSet(profileKey) (S1002)
// NOTE: If the flag was from a multiple profiles keyed by aws profile
// name i.e. `staging.oidc-client-id`, set the base value to that as
// well, `oidc-client-id`, such that input validation is satisfied.
v := viper.Get(profileKey)
viper.Set(flag, v)

return profileKey
}
return flag
}

func readConfig() (Attributes, error) {
// Side loading multiple profiles from okta.yaml file if it exists
if oktaConfig, err := OktaConfig(); err == nil {
profiles := oktaConfig.AWSCLI.PROFILES
viper.SetConfigType("yaml")
yamlData, err := yaml.Marshal(&profiles)
if err != nil {
path, _ := OktaConfigPath()
fmt.Fprintf(os.Stderr, "WARNING: error reading from %q: %+v.\n\n", path, err)
}
if err == nil {
r := bytes.NewReader(yamlData)
err = viper.MergeConfig(r)
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: error with okta.yaml %+v.\n\n", err)
}
}
}

awsProfile := viper.GetString(ProfileFlag)

attrs := Attributes{
Expand Down Expand Up @@ -808,14 +882,25 @@ func (c *Config) SetQRCode(qrCode bool) error {
return nil
}

// OktaConfig returns an Okta YAML Config object representation of $HOME/.okta/okta.yaml
func (c *Config) OktaConfig() (config *OktaYamlConfig, err error) {
homeDir, err := os.UserHomeDir()
// OktaConfigPath returns OS specific path to the okta config file, for example
// $HOME/.okta/okta.yaml
func OktaConfigPath() (path string, err error) {
var homeDir string
homeDir, err = os.UserHomeDir()
if err != nil {
return
}

configPath := filepath.Join(homeDir, DotOkta, OktaYaml)
path = filepath.Join(homeDir, DotOkta, OktaYaml)
return
}

// OktaConfig returns an Okta YAML Config object representation of $HOME/.okta/okta.yaml
func OktaConfig() (config *OktaYamlConfig, err error) {
configPath, err := OktaConfigPath()
if err != nil {
return
}
yamlConfig, err := os.ReadFile(configPath)
if err != nil {
return
Expand Down Expand Up @@ -953,6 +1038,28 @@ awscli:

fmt.Fprintf(os.Stderr, "okta.yaml \"awscli.roles\" section is a map of %d ARN string keys to friendly string label values\n", len(_roles))

profiles, ok := _awscli["profiles"]
if !ok {
fmt.Fprintf(os.Stderr, "WARNING: okta.yaml missing \"awscli.profiles\" section\n")
return
}
if profiles == nil {
fmt.Fprintf(os.Stderr, "WARNING: okta.yaml \"awscli.profiles\" section has no values\n")
return
}

_profiles, ok := profiles.(map[any]any)
if !ok {
fmt.Fprintf(os.Stderr, "WARNING: okta.yaml \"awscli.profiles\" section is not a map of separate config settings keyed by profile name\n")
return
}
if len(_profiles) == 0 {
fmt.Fprintf(os.Stderr, "WARNING: okta.yaml \"awscli.profiles\" section is an empty map of separate config settings keyed by profile name\n")
return
}

fmt.Fprintf(os.Stderr, "okta.yaml \"awscli.profiles\" section is a map of %d separate config settings keyed by profile name\n", len(_profiles))

fmt.Fprintf(os.Stderr, "okta.yaml is OK\n")
return nil
}
Expand Down
24 changes: 1 addition & 23 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"

Expand Down Expand Up @@ -59,6 +58,7 @@ type Flag struct {
//
// https://github.com/spf13/cobra/blob/main/site/content/user_guide.md#working-with-flags
func MakeFlagBindings(cmd *cobra.Command, flags []Flag, persistent bool) {
fmt.Println("MakeFlagBindings")
// bind env vars
for _, f := range flags {
_ = viper.BindEnv(f.EnvVar, f.Name)
Expand All @@ -82,28 +82,6 @@ func MakeFlagBindings(cmd *cobra.Command, flags []Flag, persistent bool) {
if vipAwsRegion != "" && os.Getenv(awsRegionEnvVar) == "" {
_ = os.Setenv(awsRegionEnvVar, vipAwsRegion)
}
} else {
// Check if .okta-aws-cli/conifg.yml exists
usr, err := user.Current()
if err == nil {
oktaConfig := filepath.Join(usr.HomeDir, ".okta-aws-cli", "config.yml")
if _, err := os.Stat(oktaConfig); err == nil || !errors.Is(err, os.ErrNotExist) {
viper.AddConfigPath(filepath.Join(usr.HomeDir, ".okta-aws-cli"))
viper.SetConfigName("config.yml")
viper.SetConfigType("yml")

_ = viper.ReadInConfig()

// After viper reads in the dotenv file check if AWS_REGION is set
// there. The value will be keyed by lower case name. If it is, set
// AWS_REGION as an ENV VAR if it hasn't already been.
awsRegionEnvVar := "AWS_REGION"
vipAwsRegion := viper.GetString(strings.ToLower(awsRegionEnvVar))
if vipAwsRegion != "" && os.Getenv(awsRegionEnvVar) == "" {
_ = os.Setenv(awsRegionEnvVar, vipAwsRegion)
}
}
}
}

viper.AutomaticEnv()
Expand Down
6 changes: 3 additions & 3 deletions internal/webssoauth/webssoauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func (w *WebSSOAuthentication) selectFedApp(apps []*okta.Application) (string, e
choices := make([]string, len(apps))
var selected string
var configIDPs map[string]string
oktaConfig, err := w.config.OktaConfig()
oktaConfig, err := config.OktaConfig()
if err == nil {
configIDPs = oktaConfig.AWSCLI.IDPS
}
Expand Down Expand Up @@ -463,7 +463,7 @@ func (w *WebSSOAuthentication) choiceFriendlyLabelRole(arn string, roles map[str

// promptForRole prompt operator for the AWS Role ARN given a slice of Role ARNs
func (w *WebSSOAuthentication) promptForRole(idp string, roleARNs []string) (roleARN string, err error) {
oktaConfig, err := w.config.OktaConfig()
oktaConfig, err := config.OktaConfig()
var configRoles map[string]string
if err == nil {
configRoles = oktaConfig.AWSCLI.ROLES
Expand Down Expand Up @@ -519,7 +519,7 @@ func (w *WebSSOAuthentication) promptForRole(idp string, roleARNs []string) (rol
// to pretty print out the IdP name again.
func (w *WebSSOAuthentication) promptForIDP(idpARNs []string) (idpARN string, err error) {
var configIDPs map[string]string
if oktaConfig, cErr := w.config.OktaConfig(); cErr == nil {
if oktaConfig, cErr := config.OktaConfig(); cErr == nil {
configIDPs = oktaConfig.AWSCLI.IDPS
}

Expand Down

0 comments on commit 91305d7

Please sign in to comment.