Skip to content

Commit

Permalink
Merge pull request #175 from okta/pr_162_MatthewJohn
Browse files Browse the repository at this point in the history
Bringing in MatthewJohn's PR #162 - multiple config profiles in okta.yaml
  • Loading branch information
monde authored Feb 14, 2024
2 parents d6f4461 + 022d17c commit 3c5a1d4
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 36 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
181 changes: 149 additions & 32 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 @@ -315,34 +365,68 @@ func NewConfig(attrs *Attributes) (*Config, error) {
return cfg, nil
}

func getFlagNameFromProfile(awsProfile string, flag string) string {
profileKey := fmt.Sprintf("%s.%s", awsProfile, flag)
if awsProfile != "" && viper.IsSet(profileKey) {
// 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{
AllProfiles: viper.GetBool(AllProfilesFlag),
AuthzID: viper.GetString(AuthzIDFlag),
AWSCredentials: viper.GetString(AWSCredentialsFlag),
AWSIAMIdP: viper.GetString(AWSIAMIdPFlag),
AWSIAMRole: viper.GetString(AWSIAMRoleFlag),
AWSSessionDuration: viper.GetInt64(SessionDurationFlag),
AWSRegion: viper.GetString(AWSRegionFlag),
CustomScope: viper.GetString(CustomScopeFlag),
Debug: viper.GetBool(DebugFlag),
DebugAPICalls: viper.GetBool(DebugAPICallsFlag),
Exec: viper.GetBool(ExecFlag),
FedAppID: viper.GetString(AWSAcctFedAppIDFlag),
Format: viper.GetString(FormatFlag),
LegacyAWSVariables: viper.GetBool(LegacyAWSVariablesFlag),
ExpiryAWSVariables: viper.GetBool(ExpiryAWSVariablesFlag),
CacheAccessToken: viper.GetBool(CacheAccessTokenFlag),
OIDCAppID: viper.GetString(OIDCClientIDFlag),
OpenBrowser: viper.GetBool(OpenBrowserFlag),
OpenBrowserCommand: viper.GetString(OpenBrowserCommandFlag),
OrgDomain: viper.GetString(OrgDomainFlag),
PrivateKey: viper.GetString(PrivateKeyFlag),
PrivateKeyFile: viper.GetString(PrivateKeyFileFlag),
KeyID: viper.GetString(KeyIDFlag),
Profile: viper.GetString(ProfileFlag),
QRCode: viper.GetBool(QRCodeFlag),
WriteAWSCredentials: viper.GetBool(WriteAWSCredentialsFlag),
AllProfiles: viper.GetBool(getFlagNameFromProfile(awsProfile, AllProfilesFlag)),
AuthzID: viper.GetString(getFlagNameFromProfile(awsProfile, AuthzIDFlag)),
AWSCredentials: viper.GetString(getFlagNameFromProfile(awsProfile, AWSCredentialsFlag)),
AWSIAMIdP: viper.GetString(getFlagNameFromProfile(awsProfile, AWSIAMIdPFlag)),
AWSIAMRole: viper.GetString(getFlagNameFromProfile(awsProfile, AWSIAMRoleFlag)),
AWSRegion: viper.GetString(getFlagNameFromProfile(awsProfile, AWSRegionFlag)),
AWSSessionDuration: viper.GetInt64(getFlagNameFromProfile(awsProfile, SessionDurationFlag)),
CustomScope: viper.GetString(getFlagNameFromProfile(awsProfile, CustomScopeFlag)),
Debug: viper.GetBool(getFlagNameFromProfile(awsProfile, DebugFlag)),
DebugAPICalls: viper.GetBool(getFlagNameFromProfile(awsProfile, DebugAPICallsFlag)),
Exec: viper.GetBool(getFlagNameFromProfile(awsProfile, ExecFlag)),
FedAppID: viper.GetString(getFlagNameFromProfile(awsProfile, AWSAcctFedAppIDFlag)),
Format: viper.GetString(getFlagNameFromProfile(awsProfile, FormatFlag)),
LegacyAWSVariables: viper.GetBool(getFlagNameFromProfile(awsProfile, LegacyAWSVariablesFlag)),
ExpiryAWSVariables: viper.GetBool(getFlagNameFromProfile(awsProfile, ExpiryAWSVariablesFlag)),
CacheAccessToken: viper.GetBool(getFlagNameFromProfile(awsProfile, CacheAccessTokenFlag)),
OIDCAppID: viper.GetString(getFlagNameFromProfile(awsProfile, OIDCClientIDFlag)),
OpenBrowser: viper.GetBool(getFlagNameFromProfile(awsProfile, OpenBrowserFlag)),
OpenBrowserCommand: viper.GetString(getFlagNameFromProfile(awsProfile, OpenBrowserCommandFlag)),
OrgDomain: viper.GetString(getFlagNameFromProfile(awsProfile, OrgDomainFlag)),
PrivateKey: viper.GetString(getFlagNameFromProfile(awsProfile, PrivateKeyFlag)),
PrivateKeyFile: viper.GetString(getFlagNameFromProfile(awsProfile, PrivateKeyFileFlag)),
KeyID: viper.GetString(getFlagNameFromProfile(awsProfile, KeyIDFlag)),
Profile: viper.GetString(getFlagNameFromProfile(awsProfile, ProfileFlag)),
QRCode: viper.GetBool(getFlagNameFromProfile(awsProfile, QRCodeFlag)),
WriteAWSCredentials: viper.GetBool(getFlagNameFromProfile(awsProfile, WriteAWSCredentialsFlag)),
}
if attrs.Format == "" {
attrs.Format = EnvVarFormat
Expand Down Expand Up @@ -798,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 @@ -943,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
1 change: 1 addition & 0 deletions internal/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func MakeFlagBindings(cmd *cobra.Command, flags []Flag, persistent bool) {
_ = os.Setenv(awsRegionEnvVar, vipAwsRegion)
}
}

viper.AutomaticEnv()

// bind cli flags
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 3c5a1d4

Please sign in to comment.