diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc880a8..67f1a0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Go uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 with: - go-version: 1.19 + go-version: 1.21 - name: Setup Go Tools run: make tools diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b78992..c36011c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: name: Set up Go uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 #v3.5.0 with: - go-version: 1.19 + go-version: 1.21 - name: Import GPG key id: import_gpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 91cfd8d..a7d5c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,13 +26,20 @@ in the naming convention for `okta-aws-cli` specific names. | `OKTA_OIDC_CLIENT_ID` | `OKTA_AWSCLI_OIDC_CLIENT_ID` | | `OKTA_AWS_ACCOUNT_FEDERATION_APP_ID` | `OKTA_AWSCLI_AWS_ACCOUNT_FEDERATION_APP_ID` | -### (Completed) Process credential provider output as JSON +### (completed) Process credential provider output as JSON Emits IAM temporary credentials as JSON in [process credentials](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html) format. -### (Complete) Execute follow-on command +``` +# In $/.aws/config +[default] + # presumes OKTA_AWSCLI_* env vars are set + credential_process = okta-aws-cli m2m --format process-credentials +``` + +### (completed) Execute follow-on command Instead of scripting and/or eval'ing `okta-aws-cli` into a shell and then running another command have `okta-aws-cli` run the command directly passing @@ -46,7 +53,7 @@ $ okta-aws-cli web \ --exec -- aws ec2 describe-instances ``` -### (Complete) Collect all roles for an AWS Fed App (IdP) at once +### (completed) Collect all roles for an AWS Fed App (IdP) at once `okta-aws-cli web` will collect all available AWS IAM Roles for a given Okta AWS Federation app (IdP) at once. This is a feature specific to writing the @@ -64,12 +71,17 @@ $ okta-aws-cli web \ --write-aws-credentials \ --all-profiles -? Choose an IdP: AWS Account Federation -Updated profile "myorg-S3-read" in credentials file "/Users/me/.aws/credentials". -Updated profile "myorg-S3-write" in credentials file "/Users/me/.aws/credentials". +Web browser will open the following URL to begin Okta device authorization for the AWS CLI + +https://test.okta.com/activate?user_code=QHDMVQTZ + +Updated profile "devorg-idp1-role1" in credentials file "/Users/me/.aws/credentials". +Updated profile "devorg-idp1-role2" in credentials file "/Users/me/.aws/credentials". +Updated profile "devorg-idp2-role1" in credentials file "/Users/me/.aws/credentials". +Updated profile "prodorg-idp1-role1" in credentials file "/Users/me/.aws/credentials". ``` -### (expected) Alternate web browser open command +### (completed) Alternate web browser open command The `web` command will open the system's default web browser when the `--open-browser` flag is present. It is convenient to have the browser open on a @@ -81,8 +93,7 @@ system an alternate open command can be specified. $ okta-aws-cli web \ --org-domain test.okta.com \ --oidc-client-id 0oa5wyqjk6Wm148fE1d7 \ - --open-browser \ - --open-browser-command "open -na 'Google Chrome' --args -incognito" + --open-browser-command "open -na \"Google\ Chrome\" --args --incognito" ``` ``` @@ -90,10 +101,13 @@ $ okta-aws-cli web \ $ okta-aws-cli web \ --org-domain test.okta.com \ --oidc-client-id 0oa5wyqjk6Wm148fE1d7 \ - --open-browser \ - --open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --profile-directory='Profile 1'" + --open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --profile-directory=\"Profile\ 1\"" ``` +## 2.0.0-beta.4 (October 12, 2023) + +`okta-aws-cli web` can have it's open browser command customized. + ## 2.0.0-beta.3 (October 10, 2023) `okta-aws-cli web` can collect all roles to an AWS credentials file for a given @@ -103,46 +117,17 @@ AWS Federation App (IdP) in one invocation of the CLI. Execute a subcommand directly from `okta-aws-cli` -``` -$ okta-aws-cli m2m --format noop --exec -- aws s3 ls s3://example - PRE aaa/ -2023-03-08 16:01:01 4 a.log -``` - ## 2.0.0-beta.1 (October 2, 2023) Support for AWS CLI [process credential provider](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html) -``` -# $/.aws/config -[default] -# presumes OKTA_AWSCLI_* env vars are set -credential_process = okta-aws-cli m2m --format process-credentials -``` - ## 2.0.0-beta.0 (September 29, 2023) -### New commands - -`okta-aws-cli`'s functions are encapsulated as (sub)commands e.g. `$ okta-aws-cli [sub-command]` - -| Command | Description | -|-----|-----| -| `web` | Human oriented retrieval of temporary IAM credentials through Okta authentication and device authorization. Note: if `okta-aws-cli` is not given a command it defaults to this original `web` command. | -| `m2m` | Machine/headless oriented retrieval of temporary IAM credentials through Okta authentication with a private key. | -| `debug` | Debug okta.yaml config file and exit. | - -### Environment variable name changes +`okta-aws-cli`'s functions are encapsulated as (sub)commands `web`, `m2m`, `debug` A small number of environment variable names have been renamed to be consistent in the naming convention for `okta-aws-cli` specific names. -| old name | new name | -|----------|----------| -| `OKTA_ORG_DOMAIN` | `OKTA_AWSCLI_ORG_DOMAIN` | -| `OKTA_OIDC_CLIENT_ID` | `OKTA_AWSCLI_OIDC_CLIENT_ID` | -| `OKTA_AWS_ACCOUNT_FEDERATION_APP_ID` | `OKTA_AWSCLI_AWS_ACCOUNT_FEDERATION_APP_ID` | - ## 1.2.2 (August 30, 2023) * Ensure evaluation of CLI flag for profile is in the same order as the other flags [#124](https://github.com/okta/okta-aws-cli/pull/124) diff --git a/README.md b/README.md index cd2955c..334410a 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,8 @@ These settings are all optional: | AWS IAM Identity Provider ARN | Preselects the IdP list to this preferred IAM Identity Provider. If there are other IdPs available they will not be listed. | `--aws-iam-idp [value]` | `OKTA_AWSCLI_IAM_IDP` | | Display QR Code | `true` if flag is present | `--qr-code` | `OKTA_AWSCLI_QR_CODE=true` | | Automatically open the activation URL with the system web browser | `true` if flag is present | `--open-browser` | `OKTA_AWSCLI_OPEN_BROWSER=true` | -| Gather all profiles for a given IdP (implies aws-credentials file output format)) | `true` if flag is present | `--all-profiles` | `OKTA_AWSCLI_OPEN_BROWSER=true` | +| Automatically open the activation URL with the given web browser command | Shell escaped browser command | `--open-browser-command [command]` | `OKTA_AWSCLI_OPEN_BROWSER_COMMAND` | +| Gather all profiles for all IdPs and Roles associated with an AWS Fed App (implies aws-credentials file output format)) | `true` if flag is present | `--all-profiles` | `OKTA_AWSCLI_OPEN_BROWSER=true` | #### Allowed Web SSO Client ID @@ -745,8 +746,9 @@ Federation app (IdP) at once. This is a feature specific to writing the aliases is available on the given role) then `-` then abbreviated role name. ``` -# AWS account alias "myorg", given IdP associated with "AWS Account Federation" -# and an app associated with two roles. +# Two Okta "AWS Account Federation" apps, one on AWS alias "prod-org", the other +# on alias "dev-org". Includes short name for the actual IAM IdPs and IAM Roles +# on the Fed App. $ okta-aws-cli web \ --org-domain test.okta.com \ @@ -755,8 +757,28 @@ $ okta-aws-cli web \ --all-profiles ? Choose an IdP: AWS Account Federation -Updated profile "myorg-S3-read" in credentials file "/Users/me/.aws/credentials". -Updated profile "myorg-S3-write" in credentials file "/Users/me/.aws/credentials". +Updated profile "dev-org-s3ops-read" in credentials file "/Users/me/.aws/credentials". +Updated profile "dev-org-s3ops-write" in credentials file "/Users/me/.aws/credentials". +Updated profile "prod-org-containerops-ec2-full" in credentials file "/Users/me/.aws/credentials". +Updated profile "prod-org-containerops-eks-full" in credentials file "/Users/me/.aws/credentials". +``` + +### Alternative open browser command + +`okta-aws-cli web` can have it's open browser command customized. + +``` +# OSX examples, the device authorization URL is appended to the browser args. + +$ okta-aws-cli web \ + --oidc-client-id abc \ + --org-domain test.okta.com \ + --open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --profile-directory=\"Profile\ 1\"" + +$ okta-aws-cli web \ + --oidc-client-id abc \ + --org-domain test.okta.com \ + --open-browser-command "open -na \"Google\ Chrome\" --args --incognito" ``` ### Help diff --git a/cmd/root/web/web.go b/cmd/root/web/web.go index d1ce7b3..24fbf01 100644 --- a/cmd/root/web/web.go +++ b/cmd/root/web/web.go @@ -54,6 +54,13 @@ var ( Usage: "Automatically open the activation URL with the system web browser", EnvVar: config.OpenBrowserEnvVar, }, + { + Name: config.OpenBrowserCommandFlag, + Short: "m", + Value: "", + Usage: "Automatically open the activation URL with the given web browser command", + EnvVar: config.OpenBrowserCommandEnvVar, + }, { Name: config.AllProfilesFlag, Short: "k", diff --git a/go.mod b/go.mod index 8371208..9deb431 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/okta/okta-aws-cli -go 1.19 +go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.6 @@ -23,7 +23,10 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) -require golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect +require ( + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index bd5bd7e..af6a05d 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= diff --git a/internal/aws/aws.go b/internal/aws/aws.go index da8d1b2..6d3abd0 100644 --- a/internal/aws/aws.go +++ b/internal/aws/aws.go @@ -21,13 +21,6 @@ import ( "time" ) -// Credential Interface to represent AWS credentials in different formats. -type Credential interface { - // Trivial function to allow concrete structs to be represented by this - // interface. - IsCredential() bool -} - // CredentialContainer denormalized struct of all the values can be presented in // the different credentials formats type CredentialContainer struct { @@ -47,9 +40,6 @@ type EnvVarCredential struct { SessionToken string } -// IsCredential env var credential is a credential -func (e *EnvVarCredential) IsCredential() bool { return true } - // CredsFileCredential representation of an AWS credential for the AWS // credentials file type CredsFileCredential struct { @@ -60,9 +50,6 @@ type CredsFileCredential struct { profile string } -// IsCredential creds file credential is a credential -func (c *CredsFileCredential) IsCredential() bool { return true } - // SetProfile sets the profile name associated with this AWS credential. func (c *CredsFileCredential) SetProfile(p string) { c.profile = p } @@ -79,9 +66,6 @@ type ProcessCredential struct { Version int `json:"Version,omitempty"` } -// IsCredential process credential is a credential -func (c *ProcessCredential) IsCredential() bool { return true } - // MarshalJSON ensure Expiration date time is formatted RFC 3339 format. func (c *ProcessCredential) MarshalJSON() ([]byte, error) { type Alias ProcessCredential @@ -101,9 +85,3 @@ func (c *ProcessCredential) MarshalJSON() ([]byte, error) { } return json.Marshal(obj) } - -// NoopCredential Convenience representation for not printing credentials -type NoopCredential struct{} - -// IsCredential noop credential is a credential -func (n *NoopCredential) IsCredential() bool { return true } diff --git a/internal/config/config.go b/internal/config/config.go index 6b877f2..f3ee145 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,7 +39,7 @@ func init() { const ( // Version app version - Version = "2.0.0-beta.3" + Version = "2.0.0-beta.4" // AWSCredentialsFormat format const AWSCredentialsFormat = "aws-credentials" @@ -76,6 +76,8 @@ const ( OIDCClientIDFlag = "oidc-client-id" // OpenBrowserFlag cli flag const OpenBrowserFlag = "open-browser" + // OpenBrowserCommandFlag cli flag const + OpenBrowserCommandFlag = "open-browser-command" // OrgDomainFlag cli flag const OrgDomainFlag = "org-domain" // PrivateKeyFlag cli flag const @@ -139,6 +141,8 @@ const ( OldOktaAWSAccountFederationAppIDEnvVar = "OKTA_AWS_ACCOUNT_FEDERATION_APP_ID" // OpenBrowserEnvVar env var const OpenBrowserEnvVar = "OKTA_AWSCLI_OPEN_BROWSER" + // OpenBrowserCommandEnvVar env var const + OpenBrowserCommandEnvVar = "OKTA_AWSCLI_OPEN_BROWSER_COMMAND" // PrivateKeyEnvVar env var const PrivateKeyEnvVar = "OKTA_AWSCLI_PRIVATE_KEY" // KeyIDEnvVar env var const @@ -200,6 +204,7 @@ type Config struct { legacyAWSVariables bool oidcAppID string openBrowser bool + openBrowserCommand string orgDomain string privateKey string profile string @@ -228,6 +233,7 @@ type Attributes struct { LegacyAWSVariables bool OIDCAppID string OpenBrowser bool + OpenBrowserCommand string OrgDomain string PrivateKey string Profile string @@ -266,6 +272,7 @@ func NewConfig(attrs *Attributes) (*Config, error) { format: attrs.Format, legacyAWSVariables: attrs.LegacyAWSVariables, openBrowser: attrs.OpenBrowser, + openBrowserCommand: attrs.OpenBrowserCommand, privateKey: attrs.PrivateKey, keyID: attrs.KeyID, profile: attrs.Profile, @@ -315,6 +322,7 @@ func readConfig() (Attributes, error) { CacheAccessToken: viper.GetBool(CacheAccessTokenFlag), OIDCAppID: viper.GetString(OIDCClientIDFlag), OpenBrowser: viper.GetBool(OpenBrowserFlag), + OpenBrowserCommand: viper.GetString(OpenBrowserCommandFlag), OrgDomain: viper.GetString(OrgDomainFlag), PrivateKey: viper.GetString(PrivateKeyFlag), KeyID: viper.GetString(KeyIDFlag), @@ -433,6 +441,11 @@ func readConfig() (Attributes, error) { if !attrs.OpenBrowser { attrs.OpenBrowser = viper.GetBool(downCase(OpenBrowserEnvVar)) } + if attrs.OpenBrowserCommand == "" { + // open browser command implies open browser + attrs.OpenBrowser = true + attrs.OpenBrowserCommand = viper.GetString(downCase(OpenBrowserCommandEnvVar)) + } if !attrs.Debug { attrs.Debug = viper.GetBool(downCase(DebugEnvVar)) } @@ -678,6 +691,17 @@ func (c *Config) SetOpenBrowser(openBrowser bool) error { return nil } +// OpenBrowserCommand -- +func (c *Config) OpenBrowserCommand() string { + return c.openBrowserCommand +} + +// SetOpenBrowserCommand -- +func (c *Config) SetOpenBrowserCommand(openBrowserCommand string) error { + c.openBrowserCommand = openBrowserCommand + return nil +} + // OrgDomain -- func (c *Config) OrgDomain() string { return c.orgDomain diff --git a/internal/output/aws_credentials_file.go b/internal/output/aws_credentials_file.go index a1a79f4..646d57a 100644 --- a/internal/output/aws_credentials_file.go +++ b/internal/output/aws_credentials_file.go @@ -183,8 +183,13 @@ func NewAWSCredentialsFile(legacyVars bool, expiryVars bool, expiry string) *AWS // Output Satisfies the Outputter interface and appends AWS credentials to // credentials file. -func (a *AWSCredentialsFile) Output(c *config.Config, oc oaws.Credential) error { - cfc := oc.(*oaws.CredsFileCredential) +func (a *AWSCredentialsFile) Output(c *config.Config, cc *oaws.CredentialContainer) error { + cfc := &oaws.CredsFileCredential{ + AccessKeyID: cc.AccessKeyID, + SecretAccessKey: cc.SecretAccessKey, + SessionToken: cc.SessionToken, + } + cfc.SetProfile(cc.Profile) if c.WriteAWSCredentials() { return a.writeConfig(c, cfc) } diff --git a/internal/output/envvar.go b/internal/output/envvar.go index 04d98ab..efc1138 100644 --- a/internal/output/envvar.go +++ b/internal/output/envvar.go @@ -18,6 +18,7 @@ package output import ( "fmt" + "os" "runtime" oaws "github.com/okta/okta-aws-cli/internal/aws" @@ -38,8 +39,13 @@ func NewEnvVar(legacyVars bool) *EnvVar { // Output Satisfies the Outputter interface and outputs AWS credentials as shell // export statements to STDOUT -func (e *EnvVar) Output(c *config.Config, oc oaws.Credential) error { - evc := oc.(*oaws.EnvVarCredential) +func (e *EnvVar) Output(c *config.Config, cc *oaws.CredentialContainer) error { + evc := &oaws.EnvVarCredential{ + AccessKeyID: cc.AccessKeyID, + SecretAccessKey: cc.SecretAccessKey, + SessionToken: cc.SessionToken, + } + fmt.Fprintf(os.Stderr, "\n") if runtime.GOOS == "windows" { fmt.Printf("setx AWS_ACCESS_KEY_ID %s\n", evc.AccessKeyID) fmt.Printf("setx AWS_SECRET_ACCESS_KEY %s\n", evc.SecretAccessKey) diff --git a/internal/output/noop.go b/internal/output/noop.go index 53c6c13..8581488 100644 --- a/internal/output/noop.go +++ b/internal/output/noop.go @@ -30,7 +30,7 @@ func NewNoopCredentials() *NoopCredentials { } // Output Satisfies the Outputter interface and outputs nothing -func (n *NoopCredentials) Output(c *config.Config, oc oaws.Credential) error { +func (n *NoopCredentials) Output(c *config.Config, cc *oaws.CredentialContainer) error { // no-op return nil } diff --git a/internal/output/output.go b/internal/output/output.go index 94b13c5..a78f6d0 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -17,8 +17,6 @@ package output import ( - "fmt" - "os" "time" oaws "github.com/okta/okta-aws-cli/internal/aws" @@ -27,7 +25,7 @@ import ( // Outputter Interface to output AWS credentials in different formats. type Outputter interface { - Output(c *config.Config, oc oaws.Credential) error + Output(c *config.Config, cc *oaws.CredentialContainer) error } // RenderAWSCredential Renders the credentials in the prescribed format. @@ -37,37 +35,12 @@ func RenderAWSCredential(cfg *config.Config, cc *oaws.CredentialContainer) error case config.AWSCredentialsFormat: expiry := time.Now().Add(time.Duration(cfg.AWSSessionDuration()) * time.Second).Format(time.RFC3339) o = NewAWSCredentialsFile(cfg.LegacyAWSVariables(), cfg.ExpiryAWSVariables(), expiry) - cfc := &oaws.CredsFileCredential{ - AccessKeyID: cc.AccessKeyID, - SecretAccessKey: cc.SecretAccessKey, - SessionToken: cc.SessionToken, - } - cfc.SetProfile(cc.Profile) - return o.Output(cfg, cfc) case config.ProcessCredentialsFormat: o = NewProcessCredentials() - pc := &oaws.ProcessCredential{ - AccessKeyID: cc.AccessKeyID, - SecretAccessKey: cc.SecretAccessKey, - SessionToken: cc.SessionToken, - Expiration: cc.Expiration, - // See AWS docs: "Note As of this writing, the Version key must be set to 1. - // This might increment over time as the structure evolves." - Version: 1, - } - return o.Output(cfg, pc) case config.NoopFormat: o = NewNoopCredentials() - nc := &oaws.NoopCredential{} - return o.Output(cfg, nc) default: o = NewEnvVar(cfg.LegacyAWSVariables()) - fmt.Fprintf(os.Stderr, "\n") - evc := &oaws.EnvVarCredential{ - AccessKeyID: cc.AccessKeyID, - SecretAccessKey: cc.SecretAccessKey, - SessionToken: cc.SessionToken, - } - return o.Output(cfg, evc) } + return o.Output(cfg, cc) } diff --git a/internal/output/process_credentials.go b/internal/output/process_credentials.go index 5dae6bb..028cd9e 100644 --- a/internal/output/process_credentials.go +++ b/internal/output/process_credentials.go @@ -35,8 +35,16 @@ func NewProcessCredentials() *ProcessCredentials { // Output Satisfies the Outputter interface and outputs AWS credentials as JSON // to STDOUT -func (p *ProcessCredentials) Output(c *config.Config, oc oaws.Credential) error { - pc := oc.(*oaws.ProcessCredential) +func (p *ProcessCredentials) Output(c *config.Config, cc *oaws.CredentialContainer) error { + pc := &oaws.ProcessCredential{ + AccessKeyID: cc.AccessKeyID, + SecretAccessKey: cc.SecretAccessKey, + SessionToken: cc.SessionToken, + Expiration: cc.Expiration, + // See AWS docs: "Note As of this writing, the Version key must be set to 1. + // This might increment over time as the structure evolves." + Version: 1, + } credJSON, err := json.MarshalIndent(pc, "", " ") if err != nil { diff --git a/internal/webssoauth/webssoauth.go b/internal/webssoauth/webssoauth.go index e8daba0..92ee3a3 100644 --- a/internal/webssoauth/webssoauth.go +++ b/internal/webssoauth/webssoauth.go @@ -27,6 +27,7 @@ import ( "net/http" "net/url" "os" + osexec "os/exec" "os/user" "path/filepath" "strings" @@ -42,6 +43,7 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" "github.com/cenkalti/backoff/v4" + "github.com/google/shlex" "github.com/mdp/qrterminal" brwsr "github.com/pkg/browser" "golang.org/x/net/html" @@ -203,6 +205,16 @@ AWS Federation App with --aws-acct-fed-app-id FED_APP_ID if len(apps) == 1 { // only one app, we don't need to prompt selection of idp / fed app fedAppID = apps[0].ID + } else if w.config.AllProfiles() { + // special case, we're going to run the table and get all profiles for all apps + errArr := []error{} + for _, app := range apps { + if err = w.establishTokenWithFedAppID(clientID, app.ID, at); err != nil { + errArr = append(errArr, err) + } + } + + return errors.Join(errArr...) } else { // Here, we do want to prompt for selection of the Fed App. // If the app is making use of "Role value pattern" on AWS settings we @@ -367,15 +379,17 @@ func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion SessionToken: *svcResp.Credentials.SessionToken, Expiration: svcResp.Credentials.Expiration, } - if w.config.Profile() != "" { + if !w.config.AllProfiles() && w.config.Profile() != "" { cc.Profile = w.config.Profile() return cc, nil } - var profileName string - var roleName string + var profileName, idpName, roleName string + if _, after, found := strings.Cut(iar.idp, "/"); found { + idpName = after + } if _, after, found := strings.Cut(iar.role, "/"); found { - roleName = "-" + after + roleName = after } sessCopy := sess.Copy(&aws.Config{ Credentials: credentials.NewStaticCredentials( @@ -385,12 +399,13 @@ func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion ), }) if p, err := w.fetchAWSAccountAlias(sessCopy); err != nil { - fmt.Fprintf(os.Stderr, "unable to determine account alias, setting profile name to %q\n", iar.idp) - profileName = iar.idp + org := "org" + fmt.Fprintf(os.Stderr, "unable to determine account alias, setting alias name to %q\n", org) + profileName = org } else { profileName = p } - cc.Profile = fmt.Sprintf("%s%s", profileName, roleName) + cc.Profile = fmt.Sprintf("%s-%s-%s", profileName, idpName, roleName) return cc, nil } @@ -691,12 +706,32 @@ func (w *WebSSOAuthentication) promptAuthentication(da *okta.DeviceAuthorization ` openMsg := "Open" if w.config.OpenBrowser() { - openMsg = "System web browser will open" + openMsg = "Web browser will open" } w.consolePrint(prompt, openMsg, qrCode, da.VerificationURIComplete) - if w.config.OpenBrowser() { + if w.config.OpenBrowserCommand() != "" { + bCmd := w.config.OpenBrowserCommand() + if bCmd != "" { + bArgs, err := splitArgs(bCmd) + if err != nil { + w.consolePrint("Browser command %q is invalid: %v\n", bCmd, err) + return + } + bArgs = append(bArgs, da.VerificationURIComplete) + cmd := osexec.Command(bArgs[0], bArgs[1:]...) + out, err := cmd.Output() + if _, ok := err.(*osexec.ExitError); ok { + w.consolePrint("Failed to open activation URL with given browser: %v\n", err) + w.consolePrint(" %s\n", strings.Join(bArgs, " ")) + } + if len(out) > 0 { + w.consolePrint("browser output:\n%s\n", string(out)) + } + } + + } else if w.config.OpenBrowser() { brwsr.Stdout = os.Stderr if err := brwsr.OpenURL(da.VerificationURIComplete); err != nil { w.consolePrint("Failed to open activation URL with system browser: %v\n", err) @@ -1081,3 +1116,7 @@ func (w *WebSSOAuthentication) fetchAWSAccountAlias(sess *session.Session) (stri } return *svcResp.AccountAliases[0], nil } + +func splitArgs(args string) ([]string, error) { + return shlex.Split(args) +} diff --git a/internal/webssoauth/webssoauth_test.go b/internal/webssoauth/webssoauth_test.go index cdd3bb7..4a695fb 100644 --- a/internal/webssoauth/webssoauth_test.go +++ b/internal/webssoauth/webssoauth_test.go @@ -19,6 +19,7 @@ package webssoauth import ( "net/http" "os" + "reflect" "testing" "github.com/okta/okta-aws-cli/internal/config" @@ -97,3 +98,50 @@ func setupTest(t *testing.T) (*config.Config, func(t *testing.T)) { return config, tearDown } + +func TestOpenBrowserCommandSplitArgs(t *testing.T) { + testCases := []struct { + name string + command string + expected []string + }{ + { + name: "osx open", + command: `open`, + expected: []string{"open"}, + }, + { + name: "osx open named app google chrome in incognito mode", + command: `open -na "Google Chrome" --args --incognito`, + expected: []string{ + "open", + "-na", + "Google Chrome", + "--args", + "--incognito", + }, + }, + { + name: "osx open named app google chrome in incognito mode", + command: `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --profile-directory=\"Person\ 1\"`, + expected: []string{ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + `--profile-directory="Person 1"`, + }, + }, + } + t.Parallel() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := splitArgs(tc.command) + if err != nil { + t.Errorf("didn't expect error for command %q: %+v", tc.command, err) + return + } + equal := reflect.DeepEqual(result, tc.expected) + if !equal { + t.Errorf("expected %+v to equal %+v", tc.expected, result) + } + }) + } +}