Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bringing in MatthewJohn's PR #162 - multiple config profiles in okta.yaml #175

Merged
merged 10 commits into from
Feb 14, 2024
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
Loading