From 9722e542b00e207cc0326a8d57c42e8b0e59d481 Mon Sep 17 00:00:00 2001 From: Mike Mondragon Date: Wed, 4 Oct 2023 22:45:01 -0700 Subject: [PATCH 1/2] Add 'exec' subcommand to avoid writing credentials to disk or injecting into the shell Closes #135 --- CHANGELOG.md | 14 +++- README.md | 42 ++++++++++- cmd/root/root.go | 7 ++ internal/config/config.go | 24 ++++++ internal/exec/exec.go | 97 +++++++++++++++++++++++++ internal/m2mauth/m2mauth.go | 27 +++++-- internal/output/aws_credentials_file.go | 24 +++--- internal/output/envvar.go | 20 ++--- internal/output/noop.go | 36 +++++++++ internal/output/output.go | 10 ++- internal/output/process_credentials.go | 8 +- internal/webssoauth/webssoauth.go | 30 ++++++-- 12 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 internal/exec/exec.go create mode 100644 internal/output/noop.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a647cfd..de95935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,14 +32,14 @@ Emits IAM temporary credentials as JSON in [process credentials](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html) format. -### (expected) Secondary command exec +### (Complete) 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 along the IAM credentials as environment variables. ``` -# CLI exec's anything after the double dash "--" as another command. +# CLI exec's anything after the double dash "--" arguments terminator as another command. $ okta-aws-cli web \ --org-domain test.okta.com \ --oidc-client-id 0oa5wyqjk6Wm148fE1d7 \ @@ -71,6 +71,16 @@ $ okta-aws-cli web \ --open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --profile-directory='Profile 1'" ``` +## 2.0.0-beta.2 (October 5, 2023) + +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) diff --git a/README.md b/README.md index 489a314..e9a8494 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ These global settings are optional unless marked otherwise: | OIDC Client ID (**required**) | For `web` the OIDC native application / [Allowed Web SSO Client ID](#allowed-web-sso-client-id), for `m2m` the API services app ID | `--oidc-client-id [value]` | `OKTA_AWSCLI_OIDC_CLIENT_ID` | | AWS IAM Role ARN (**optional** for `web`, **required** for `m2m`) | For web preselects the role list to this preferred IAM role for the given IAM Identity Provider. For `m2m` | `--aws-iam-role [value]` | `OKTA_AWSCLI_IAM_ROLE` | | AWS Session Duration | The lifetime, in seconds, of the AWS credentials. Must be between 60 and 43200. | `--session-duration [value]` | `OKTA_AWSCLI_SESSION_DURATION` | -| Output format | Default is `env-var`. Options: `env-var` for output to environment variables, `aws-credentials` for output to AWS credentials file, `process-credentials` for credentials as JSON | `--format [value]` | `OKTA_AWSCLI_FORMAT` | +| Output format | Default is `env-var`. Options: `env-var` for output to environment variables, `aws-credentials` for output to AWS credentials file, `process-credentials` for credentials as JSON, or `noop` for no output which can be useful with `--exec` | `--format [value]` | `OKTA_AWSCLI_FORMAT` | | Profile | Default is `default` | `--profile [value]` | `OKTA_AWSCLI_PROFILE` | | Cache Okta access token at `$HOME/.okta/awscli-access-token.json` to reduce need to open device authorization URL | `true` if flag is present | `--cache-access-token` | `OKTA_AWSCLI_CACHE_ACCESS_TOKEN=true` | | Alternate AWS credentials file path | Path to alternative credentials file other than AWS CLI default | `--aws-credentials` | `OKTA_AWSCLI_AWS_CREDENTIALS` | @@ -372,6 +372,7 @@ These global settings are optional unless marked otherwise: | Print operational information to the screen for debugging purposes | `true` if flag is present | `--debug` | `OKTA_AWSCLI_DEBUG=true` | | Verbosely print all API calls/responses to the screen | `true` if flag is present | `--debug-api-calls` | `OKTA_AWSCLI_DEBUG_API_CALLS=true` | | HTTP/HTTPS Proxy support | HTTP/HTTPS URL of proxy service (based on golang [net/http/httpproxy](https://pkg.go.dev/golang.org/x/net/http/httpproxy) package) | n/a | `HTTP_PROXY` or `HTTPS_PROXY` | +| Execute arguments after CLI arg terminator `--` as a separate process. Process will be executed with AWS cred values as AWS env vars `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`. | `true` if flag is present | `--exec` | `OKTA_AWSCLI_EXEC=true` | ### Web command settings @@ -696,6 +697,45 @@ Web example: `credential_process = okta-aws-cli web --format process-credentials --oidc-client-id abc --org-domain test.okat.com --aws-iam-idp arn:aws:iam::123:saml-provider/my-idp --aws-iam-role arn:aws:iam::294719231913:role/s3 --open-browser` +### Execute follow-on process + +`okta-aws-cli` can execute a process after it has collected credentials. It will +do so with any existing env vars prefaced by `AWS_` such as `AWS_REGION` and +also append the env vars for the new AWS credentials `AWS_ACCESS_KEY_ID`, +`AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`. Use `noop` format so `aws-aws-cli` +doesn't emit credentials to stdeout itself but passes them to the process it +executes. The output from the process will be printed to the screen properly to +STDOUT, and also STDERR if the process also writes to STDERR. + +Example 1 + +``` +$ okta-aws-cli m2m --format noop --exec -- printenv +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID=ASIAUJHVCS6UYRTRTSQE +AWS_SECRET_ACCESS_KEY=TmvLOM/doSWfmIMK... +AWS_SESSION_TOKEN=FwoGZXIvYXdzEF8aDKrf... +``` + +Example 2 + +``` +$ okta-aws-cli m2m --format noop --exec -- aws s3 ls s3://example + PRE aaa/ +2023-03-08 16:01:01 4 a.log +``` + +Example 3 (process had error and also writes to STDERR) + +``` +$ okta-aws-cli m2m --format noop --exec -- aws s3 mb s3://no-access-example +error running process +aws s3 mb s3://yz-nomad-og +make_bucket failed: s3://no-access-example An error occurred (AccessDenied) when calling the CreateBucket operation: Access Denied + +Error: exit status 1 +``` + ### Help ```shell diff --git a/cmd/root/root.go b/cmd/root/root.go index bb60b15..57f9f6d 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -134,6 +134,13 @@ func init() { Usage: "Verbosely print all API calls/responses to the screen", EnvVar: config.DebugAPICallsEnvVar, }, + { + Name: config.ExecFlag, + Short: "j", + Value: false, + Usage: "Execute any shell commands after the '--' CLI arguments termination", + EnvVar: config.ExecEnvVar, + }, } rootCmd = NewRootCommand() diff --git a/internal/config/config.go b/internal/config/config.go index 00cfc23..913b533 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,6 +47,8 @@ const ( EnvVarFormat = "env-var" // ProcessCredentialsFormat format const ProcessCredentialsFormat = "process-credentials" + // NoopFormat format const + NoopFormat = "noop" // AuthzIDFlag cli flag const AuthzIDFlag = "authz-id" @@ -64,6 +66,8 @@ const ( DebugFlag = "debug" // DebugAPICallsFlag cli flag const DebugAPICallsFlag = "debug-api-calls" + // ExecFlag cli flag const + ExecFlag = "exec" // FormatFlag cli flag const FormatFlag = "format" // OIDCClientIDFlag cli flag const @@ -111,6 +115,8 @@ const ( DebugAPICallsEnvVar = "OKTA_AWSCLI_DEBUG_API_CALLS" // ExpiryAWSVariablesEnvVar env var const ExpiryAWSVariablesEnvVar = "OKTA_AWSCLI_EXPIRY_AWS_VARIABLES" + // ExecEnvVar env var const + ExecEnvVar = "OKTA_AWSCLI_EXEC" // FormatEnvVar env var const FormatEnvVar = "OKTA_AWSCLI_FORMAT" // LegacyAWSVariablesEnvVar env var const @@ -174,6 +180,7 @@ type Config struct { customScope string debug bool debugAPICalls bool + exec bool expiryAWSVariables bool fedAppID string format string @@ -201,6 +208,7 @@ type Attributes struct { CustomScope string Debug bool DebugAPICalls bool + Exec bool ExpiryAWSVariables bool FedAppID string Format string @@ -240,6 +248,7 @@ func NewConfig(attrs *Attributes) (*Config, error) { debug: attrs.Debug, debugAPICalls: attrs.DebugAPICalls, expiryAWSVariables: attrs.ExpiryAWSVariables, + exec: attrs.Exec, fedAppID: attrs.FedAppID, format: attrs.Format, legacyAWSVariables: attrs.LegacyAWSVariables, @@ -284,6 +293,7 @@ func readConfig() (Attributes, error) { 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), @@ -407,6 +417,9 @@ func readConfig() (Attributes, error) { if !attrs.CacheAccessToken { attrs.CacheAccessToken = viper.GetBool(downCase(CacheAccessTokenEnvVar)) } + if !attrs.Exec { + attrs.Exec = viper.GetBool(downCase(ExecEnvVar)) + } return attrs, nil } @@ -535,6 +548,17 @@ func (c *Config) SetDebugAPICalls(debugAPICalls bool) error { return nil } +// Exec -- +func (c *Config) Exec() bool { + return c.exec +} + +// SetExec -- +func (c *Config) SetExec(exec bool) error { + c.exec = exec + return nil +} + // ExpiryAWSVariables -- func (c *Config) ExpiryAWSVariables() bool { return c.expiryAWSVariables diff --git a/internal/exec/exec.go b/internal/exec/exec.go new file mode 100644 index 0000000..b8759a2 --- /dev/null +++ b/internal/exec/exec.go @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023-Present, Okta, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package exec + +import ( + "fmt" + "os" + osexec "os/exec" + "strings" + + oaws "github.com/okta/okta-aws-cli/internal/aws" +) + +// Exec is a executor / a process runner +type Exec struct { + name string + args []string +} + +// NewExec Create a new executor +func NewExec() (*Exec, error) { + args := []string{} + foundArgs := false + for _, arg := range os.Args { + if arg == "--" { + foundArgs = true + continue + } + if !foundArgs { + continue + } + + args = append(args, arg) + } + + if len(args) < 1 { + return nil, fmt.Errorf("there must be at least one additional argument after the '--' CLI argument terminator") + } + + name := args[0] + args = args[1:] + ex := &Exec{ + name: name, + args: args, + } + + return ex, nil +} + +// Run Run the executor +func (e *Exec) Run(oc *oaws.Credential) error { + pairs := map[string]string{} + // pre-populate pairs with any existing env var starting with "AWS_" + for _, kv := range os.Environ() { + pair := strings.SplitN(kv, "=", 2) + k := pair[0] + if strings.HasPrefix(k, "AWS_") { + pairs[k] = pair[1] + } + } + // add creds env var names to pairs + pairs["AWS_ACCESS_KEY_ID"] = oc.AccessKeyID + pairs["AWS_SECRET_ACCESS_KEY"] = oc.SecretAccessKey + pairs["AWS_SESSION_TOKEN"] = oc.SessionToken + + cmd := osexec.Command(e.name, e.args...) + for k, v := range pairs { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) + } + + out, err := cmd.Output() + if ee, ok := err.(*osexec.ExitError); ok { + fmt.Fprintf(os.Stderr, "error running process\n") + fmt.Fprintf(os.Stderr, "%s %s\n", e.name, strings.Join(e.args, " ")) + fmt.Fprintf(os.Stderr, "%s\n", ee.Stderr) + } + if err != nil { + return err + } + + fmt.Printf("%s", string(out)) + return nil +} diff --git a/internal/m2mauth/m2mauth.go b/internal/m2mauth/m2mauth.go index f269bbd..f3079ca 100644 --- a/internal/m2mauth/m2mauth.go +++ b/internal/m2mauth/m2mauth.go @@ -35,6 +35,7 @@ import ( "github.com/aws/aws-sdk-go/service/sts" oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" + "github.com/okta/okta-aws-cli/internal/exec" "github.com/okta/okta-aws-cli/internal/okta" "github.com/okta/okta-aws-cli/internal/output" "github.com/okta/okta-aws-cli/internal/utils" @@ -55,17 +56,24 @@ type M2MAuthentication struct { } // NewM2MAuthentication New M2M Authentication constructor -func NewM2MAuthentication(config *config.Config) (*M2MAuthentication, error) { +func NewM2MAuthentication(cfg *config.Config) (*M2MAuthentication, error) { // need to set our config defaults - if config.CustomScope() == "" { - _ = config.SetCustomScope(DefaultScope) + if cfg.CustomScope() == "" { + _ = cfg.SetCustomScope(DefaultScope) } - if config.AuthzID() == "" { - _ = config.SetAuthzID(DefaultAuthzID) + if cfg.AuthzID() == "" { + _ = cfg.SetAuthzID(DefaultAuthzID) + } + + // Check if exec arg is present and that there are args for it before doing any work + if cfg.Exec() { + if _, err := exec.NewExec(); err != nil { + return nil, err + } } m := M2MAuthentication{ - config: config, + config: cfg, } return &m, nil } @@ -93,6 +101,13 @@ func (m *M2MAuthentication) EstablishIAMCredentials() error { return err } + if m.config.Exec() { + exe, _ := exec.NewExec() + if err := exe.Run(cred); err != nil { + return err + } + } + return nil } diff --git a/internal/output/aws_credentials_file.go b/internal/output/aws_credentials_file.go index b8647e4..c54ecaa 100644 --- a/internal/output/aws_credentials_file.go +++ b/internal/output/aws_credentials_file.go @@ -27,7 +27,7 @@ import ( "github.com/pkg/errors" "gopkg.in/ini.v1" - "github.com/okta/okta-aws-cli/internal/aws" + oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -64,7 +64,7 @@ func ensureConfigExists(filename string, profile string) error { return nil } -func saveProfile(filename, profile string, awsCreds *aws.Credential, legacyVars, expiryVars bool, expiry string) error { +func saveProfile(filename, profile string, awsCreds *oaws.Credential, legacyVars, expiryVars bool, expiry string) error { config, err := updateConfig(filename, profile, awsCreds, legacyVars, expiryVars, expiry) if err != nil { return err @@ -79,7 +79,7 @@ func saveProfile(filename, profile string, awsCreds *aws.Credential, legacyVars, return nil } -func updateConfig(filename, profile string, awsCreds *aws.Credential, legacyVars, expiryVars bool, expiry string) (config *ini.File, err error) { +func updateConfig(filename, profile string, awsCreds *oaws.Credential, legacyVars, expiryVars bool, expiry string) (config *ini.File, err error) { config, err = ini.Load(filename) if err != nil { return @@ -90,7 +90,7 @@ func updateConfig(filename, profile string, awsCreds *aws.Credential, legacyVars return } - builder := dynamicstruct.ExtendStruct(aws.Credential{}) + builder := dynamicstruct.ExtendStruct(oaws.Credential{}) if expiryVars { builder.AddField(ExpirationField, "", `ini:"x_security_token_expires"`) @@ -178,15 +178,15 @@ func NewAWSCredentialsFile(legacyVars bool, expiryVars bool, expiry string) *AWS // Output Satisfies the Outputter interface and appends AWS credentials to // credentials file. -func (e *AWSCredentialsFile) Output(c *config.Config, ac *aws.Credential) error { +func (e *AWSCredentialsFile) Output(c *config.Config, oc *oaws.Credential) error { if c.WriteAWSCredentials() { - return e.writeConfig(c, ac) + return e.writeConfig(c, oc) } - return e.appendConfig(c, ac) + return e.appendConfig(c, oc) } -func (e *AWSCredentialsFile) appendConfig(c *config.Config, ac *aws.Credential) error { +func (e *AWSCredentialsFile) appendConfig(c *config.Config, oc *oaws.Credential) error { f, err := os.OpenFile(c.AWSCredentials(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { return err @@ -201,11 +201,11 @@ aws_access_key_id = %s aws_secret_access_key = %s aws_session_token = %s ` - credArgs := []interface{}{c.Profile(), ac.AccessKeyID, ac.SecretAccessKey, ac.SessionToken} + credArgs := []interface{}{c.Profile(), oc.AccessKeyID, oc.SecretAccessKey, oc.SessionToken} if e.LegacyAWSVariables { creds = fmt.Sprintf("%saws_security_token = %%s\n", creds) - credArgs = append(credArgs, ac.SessionToken) + credArgs = append(credArgs, oc.SessionToken) } if e.ExpiryAWSVariables { @@ -226,7 +226,7 @@ aws_session_token = %s return nil } -func (e *AWSCredentialsFile) writeConfig(c *config.Config, ac *aws.Credential) error { +func (e *AWSCredentialsFile) writeConfig(c *config.Config, oc *oaws.Credential) error { filename := c.AWSCredentials() profile := c.Profile() @@ -235,7 +235,7 @@ func (e *AWSCredentialsFile) writeConfig(c *config.Config, ac *aws.Credential) e return err } - return saveProfile(filename, profile, ac, e.LegacyAWSVariables, e.ExpiryAWSVariables, e.Expiry) + return saveProfile(filename, profile, oc, e.LegacyAWSVariables, e.ExpiryAWSVariables, e.Expiry) } func contains(ignore []string, name string) bool { diff --git a/internal/output/envvar.go b/internal/output/envvar.go index eea86f3..b82ce69 100644 --- a/internal/output/envvar.go +++ b/internal/output/envvar.go @@ -20,7 +20,7 @@ import ( "fmt" "runtime" - "github.com/okta/okta-aws-cli/internal/aws" + oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -38,20 +38,20 @@ 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, ac *aws.Credential) error { +func (e *EnvVar) Output(c *config.Config, oc *oaws.Credential) error { if runtime.GOOS == "windows" { - fmt.Printf("setx AWS_ACCESS_KEY_ID %s\n", ac.AccessKeyID) - fmt.Printf("setx AWS_SECRET_ACCESS_KEY %s\n", ac.SecretAccessKey) - fmt.Printf("setx AWS_SESSION_TOKEN %s\n", ac.SessionToken) + fmt.Printf("setx AWS_ACCESS_KEY_ID %s\n", oc.AccessKeyID) + fmt.Printf("setx AWS_SECRET_ACCESS_KEY %s\n", oc.SecretAccessKey) + fmt.Printf("setx AWS_SESSION_TOKEN %s\n", oc.SessionToken) if e.LegacyAWSVariables { - fmt.Printf("setx AWS_SECURITY_TOKEN %s\n", ac.SessionToken) + fmt.Printf("setx AWS_SECURITY_TOKEN %s\n", oc.SessionToken) } } else { - fmt.Printf("export AWS_ACCESS_KEY_ID=%s\n", ac.AccessKeyID) - fmt.Printf("export AWS_SECRET_ACCESS_KEY=%s\n", ac.SecretAccessKey) - fmt.Printf("export AWS_SESSION_TOKEN=%s\n", ac.SessionToken) + fmt.Printf("export AWS_ACCESS_KEY_ID=%s\n", oc.AccessKeyID) + fmt.Printf("export AWS_SECRET_ACCESS_KEY=%s\n", oc.SecretAccessKey) + fmt.Printf("export AWS_SESSION_TOKEN=%s\n", oc.SessionToken) if e.LegacyAWSVariables { - fmt.Printf("export AWS_SECURITY_TOKEN=%s\n", ac.SessionToken) + fmt.Printf("export AWS_SECURITY_TOKEN=%s\n", oc.SessionToken) } } diff --git a/internal/output/noop.go b/internal/output/noop.go new file mode 100644 index 0000000..134506a --- /dev/null +++ b/internal/output/noop.go @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023-Present, Okta, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +package output + +import ( + oaws "github.com/okta/okta-aws-cli/internal/aws" + "github.com/okta/okta-aws-cli/internal/config" +) + +// NoopCredentials Don't output credentials +type NoopCredentials struct{} + +// NewNoopCredentials Creates a new NoopCredentials +func NewNoopCredentials() *NoopCredentials { + return &NoopCredentials{} +} + +// Output Satisfies the Outputter interface and outputs nothing +func (n *NoopCredentials) Output(c *config.Config, oc *oaws.Credential) error { + // no-op + return nil +} diff --git a/internal/output/output.go b/internal/output/output.go index ec8b561..09506e7 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -21,17 +21,17 @@ import ( "os" "time" - "github.com/okta/okta-aws-cli/internal/aws" + oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) // Outputter Interface to output AWS credentials in different formats. type Outputter interface { - Output(c *config.Config, ac *aws.Credential) error + Output(c *config.Config, oc *oaws.Credential) error } // RenderAWSCredential Renders the credentials in the prescribed format. -func RenderAWSCredential(cfg *config.Config, ac *aws.Credential) error { +func RenderAWSCredential(cfg *config.Config, oc *oaws.Credential) error { var o Outputter switch cfg.Format() { case config.AWSCredentialsFormat: @@ -39,10 +39,12 @@ func RenderAWSCredential(cfg *config.Config, ac *aws.Credential) error { o = NewAWSCredentialsFile(cfg.LegacyAWSVariables(), cfg.ExpiryAWSVariables(), expiry) case config.ProcessCredentialsFormat: o = NewProcessCredentials() + case config.NoopFormat: + o = NewNoopCredentials() default: o = NewEnvVar(cfg.LegacyAWSVariables()) fmt.Fprintf(os.Stderr, "\n") } - return o.Output(cfg, ac) + return o.Output(cfg, oc) } diff --git a/internal/output/process_credentials.go b/internal/output/process_credentials.go index a9f1053..d496721 100644 --- a/internal/output/process_credentials.go +++ b/internal/output/process_credentials.go @@ -20,7 +20,7 @@ import ( "encoding/json" "fmt" - "github.com/okta/okta-aws-cli/internal/aws" + oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -35,12 +35,12 @@ func NewProcessCredentials() *ProcessCredentials { // Output Satisfies the Outputter interface and outputs AWS credentials as JSON // to STDOUT -func (p *ProcessCredentials) Output(c *config.Config, ac *aws.Credential) error { +func (p *ProcessCredentials) Output(c *config.Config, oc *oaws.Credential) error { // 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." - ac.Version = 1 + oc.Version = 1 - credJSON, err := json.MarshalIndent(ac, "", " ") + credJSON, err := json.MarshalIndent(oc, "", " ") if err != nil { return err } diff --git a/internal/webssoauth/webssoauth.go b/internal/webssoauth/webssoauth.go index 931cc6c..7b4cf34 100644 --- a/internal/webssoauth/webssoauth.go +++ b/internal/webssoauth/webssoauth.go @@ -46,6 +46,7 @@ import ( oaws "github.com/okta/okta-aws-cli/internal/aws" boff "github.com/okta/okta-aws-cli/internal/backoff" "github.com/okta/okta-aws-cli/internal/config" + "github.com/okta/okta-aws-cli/internal/exec" "github.com/okta/okta-aws-cli/internal/okta" "github.com/okta/okta-aws-cli/internal/output" "github.com/okta/okta-aws-cli/internal/utils" @@ -124,6 +125,14 @@ func NewWebSSOAuthentication(cfg *config.Config) (token *WebSSOAuthentication, e return nil, fmt.Errorf("arguments --%s , --%s , and --%s must be set for %q format", config.AWSIAMIdPFlag, config.AWSIAMRoleFlag, config.OpenBrowserFlag, cfg.Format()) } } + + // Check if exec arg is present and that there are args for it before doing any work + if cfg.Exec() { + if _, err := exec.NewExec(); err != nil { + return nil, err + } + } + return token, nil } @@ -294,26 +303,33 @@ func (w *WebSSOAuthentication) establishTokenWithFedAppID(clientID, fedAppID str return err } - cred, err := w.awsAssumeRoleWithSAML(iar, assertion) + oc, err := w.awsAssumeRoleWithSAML(iar, assertion) if err != nil { return err } - err = output.RenderAWSCredential(w.config, cred) + err = output.RenderAWSCredential(w.config, oc) if err != nil { return err } + if w.config.Exec() { + exe, _ := exec.NewExec() + if err := exe.Run(oc); err != nil { + return err + } + } + return nil } // awsAssumeRoleWithSAML Get AWS Credentials with an STS Assume Role With SAML AWS // API call. -func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion string) (credential *oaws.Credential, err error) { +func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion string) (oc *oaws.Credential, err error) { awsCfg := aws.NewConfig().WithHTTPClient(w.config.HTTPClient()) sess, err := session.NewSession(awsCfg) if err != nil { - return nil, err + return } svc := sts.New(sess) input := &sts.AssumeRoleWithSAMLInput{ @@ -324,16 +340,16 @@ func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion } svcResp, err := svc.AssumeRoleWithSAML(input) if err != nil { - return nil, err + return } - credential = &oaws.Credential{ + oc = &oaws.Credential{ AccessKeyID: *svcResp.Credentials.AccessKeyId, SecretAccessKey: *svcResp.Credentials.SecretAccessKey, SessionToken: *svcResp.Credentials.SessionToken, Expiration: svcResp.Credentials.Expiration, } - return credential, nil + return oc, nil } // choiceFriendlyLabelRole returns a friendly choice for pretty printing Role From 36c0373f57e58e0e907a36749be800a471898275 Mon Sep 17 00:00:00 2001 From: Mike Mondragon Date: Thu, 5 Oct 2023 13:07:18 -0700 Subject: [PATCH 2/2] Writing the aws creds file became broken with all the other v2 work. Discussed in issue #114 --- internal/aws/aws.go | 19 ++++++++++++------- internal/aws/aws_test.go | 2 +- internal/exec/exec.go | 3 ++- internal/m2mauth/m2mauth.go | 17 ++++++++--------- internal/output/aws_credentials_file.go | 3 ++- internal/output/envvar.go | 3 ++- internal/output/noop.go | 3 ++- internal/output/output.go | 7 ++++--- internal/output/process_credentials.go | 11 ++++++++--- internal/output/process_credentials_test.go | 2 +- internal/utils/utils.go | 2 ++ internal/webssoauth/webssoauth.go | 11 +++++------ 12 files changed, 49 insertions(+), 34 deletions(-) diff --git a/internal/aws/aws.go b/internal/aws/aws.go index c1d0af2..be04925 100644 --- a/internal/aws/aws.go +++ b/internal/aws/aws.go @@ -23,16 +23,21 @@ import ( // Credential Convenience representation of an AWS credential. type Credential struct { - AccessKeyID string `ini:"aws_access_key_id" json:"AccessKeyId,omitempty"` - SecretAccessKey string `ini:"aws_secret_access_key" json:"SecretAccessKey,omitempty"` - SessionToken string `ini:"aws_session_token" json:"SessionToken,omitempty"` - Version int `ini:"aws_version" json:"Version,omitempty"` - Expiration *time.Time `ini:"aws_expiration" json:"Expiration,omitempty"` + AccessKeyID string `ini:"aws_access_key_id" json:"AccessKeyId,omitempty"` + SecretAccessKey string `ini:"aws_secret_access_key" json:"SecretAccessKey,omitempty"` + SessionToken string `ini:"aws_session_token" json:"SessionToken,omitempty"` +} + +// ProcessCredential Convenience representation of an AWS credential used for process credential format. +type ProcessCredential struct { + Credential + Version int `json:"Version,omitempty"` + Expiration *time.Time `json:"Expiration,omitempty"` } // MarshalJSON ensure Expiration date time is formatted RFC 3339 format. -func (c *Credential) MarshalJSON() ([]byte, error) { - type Alias Credential +func (c *ProcessCredential) MarshalJSON() ([]byte, error) { + type Alias ProcessCredential var exp string if c.Expiration != nil { exp = c.Expiration.Format(time.RFC3339) diff --git a/internal/aws/aws_test.go b/internal/aws/aws_test.go index eb68a8f..32c336b 100644 --- a/internal/aws/aws_test.go +++ b/internal/aws/aws_test.go @@ -26,7 +26,7 @@ import ( func TestCredentialJSON(t *testing.T) { hbtGo := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - c := Credential{ + c := ProcessCredential{ Expiration: &hbtGo, } credStr, err := json.Marshal(c) diff --git a/internal/exec/exec.go b/internal/exec/exec.go index b8759a2..abf42a3 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -23,6 +23,7 @@ import ( "strings" oaws "github.com/okta/okta-aws-cli/internal/aws" + "github.com/okta/okta-aws-cli/internal/utils" ) // Exec is a executor / a process runner @@ -86,7 +87,7 @@ func (e *Exec) Run(oc *oaws.Credential) error { if ee, ok := err.(*osexec.ExitError); ok { fmt.Fprintf(os.Stderr, "error running process\n") fmt.Fprintf(os.Stderr, "%s %s\n", e.name, strings.Join(e.args, " ")) - fmt.Fprintf(os.Stderr, "%s\n", ee.Stderr) + fmt.Fprintf(os.Stderr, utils.PassThroughStringNewLineFMT, ee.Stderr) } if err != nil { return err diff --git a/internal/m2mauth/m2mauth.go b/internal/m2mauth/m2mauth.go index f3079ca..299322c 100644 --- a/internal/m2mauth/m2mauth.go +++ b/internal/m2mauth/m2mauth.go @@ -91,19 +91,19 @@ func (m *M2MAuthentication) EstablishIAMCredentials() error { return err } - cred, err := m.awsAssumeRoleWithWebIdentity(at) + oc, ac, err := m.awsAssumeRoleWithWebIdentity(at) if err != nil { return err } - err = output.RenderAWSCredential(m.config, cred) + err = output.RenderAWSCredential(m.config, oc, ac) if err != nil { return err } if m.config.Exec() { exe, _ := exec.NewExec() - if err := exe.Run(cred); err != nil { + if err := exe.Run(oc); err != nil { return err } } @@ -111,11 +111,11 @@ func (m *M2MAuthentication) EstablishIAMCredentials() error { return nil } -func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (credential *oaws.Credential, err error) { +func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (oc *oaws.Credential, ac *sts.Credentials, err error) { awsCfg := aws.NewConfig().WithHTTPClient(m.config.HTTPClient()) sess, err := session.NewSession(awsCfg) if err != nil { - return nil, err + return } svc := sts.New(sess) @@ -127,17 +127,16 @@ func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) ( } svcResp, err := svc.AssumeRoleWithWebIdentity(input) if err != nil { - return nil, err + return } - credential = &oaws.Credential{ + oc = &oaws.Credential{ AccessKeyID: *svcResp.Credentials.AccessKeyId, SecretAccessKey: *svcResp.Credentials.SecretAccessKey, SessionToken: *svcResp.Credentials.SessionToken, - Expiration: svcResp.Credentials.Expiration, } - return credential, nil + return oc, svcResp.Credentials, nil } func (m *M2MAuthentication) createKeySigner() (jose.Signer, error) { diff --git a/internal/output/aws_credentials_file.go b/internal/output/aws_credentials_file.go index c54ecaa..1d18e77 100644 --- a/internal/output/aws_credentials_file.go +++ b/internal/output/aws_credentials_file.go @@ -23,6 +23,7 @@ import ( "reflect" "strings" + "github.com/aws/aws-sdk-go/service/sts" dynamicstruct "github.com/ompluscator/dynamic-struct" "github.com/pkg/errors" "gopkg.in/ini.v1" @@ -178,7 +179,7 @@ func NewAWSCredentialsFile(legacyVars bool, expiryVars bool, expiry string) *AWS // Output Satisfies the Outputter interface and appends AWS credentials to // credentials file. -func (e *AWSCredentialsFile) Output(c *config.Config, oc *oaws.Credential) error { +func (e *AWSCredentialsFile) Output(c *config.Config, oc *oaws.Credential, ac *sts.Credentials) error { if c.WriteAWSCredentials() { return e.writeConfig(c, oc) } diff --git a/internal/output/envvar.go b/internal/output/envvar.go index b82ce69..2d66d69 100644 --- a/internal/output/envvar.go +++ b/internal/output/envvar.go @@ -20,6 +20,7 @@ import ( "fmt" "runtime" + "github.com/aws/aws-sdk-go/service/sts" oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -38,7 +39,7 @@ 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 { +func (e *EnvVar) Output(c *config.Config, oc *oaws.Credential, ac *sts.Credentials) error { if runtime.GOOS == "windows" { fmt.Printf("setx AWS_ACCESS_KEY_ID %s\n", oc.AccessKeyID) fmt.Printf("setx AWS_SECRET_ACCESS_KEY %s\n", oc.SecretAccessKey) diff --git a/internal/output/noop.go b/internal/output/noop.go index 134506a..a230d2d 100644 --- a/internal/output/noop.go +++ b/internal/output/noop.go @@ -17,6 +17,7 @@ package output import ( + "github.com/aws/aws-sdk-go/service/sts" oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -30,7 +31,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, oc *oaws.Credential, ac *sts.Credentials) error { // no-op return nil } diff --git a/internal/output/output.go b/internal/output/output.go index 09506e7..b6b9a43 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -21,17 +21,18 @@ import ( "os" "time" + "github.com/aws/aws-sdk-go/service/sts" oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) // Outputter Interface to output AWS credentials in different formats. type Outputter interface { - Output(c *config.Config, oc *oaws.Credential) error + Output(c *config.Config, oc *oaws.Credential, ac *sts.Credentials) error } // RenderAWSCredential Renders the credentials in the prescribed format. -func RenderAWSCredential(cfg *config.Config, oc *oaws.Credential) error { +func RenderAWSCredential(cfg *config.Config, oc *oaws.Credential, ac *sts.Credentials) error { var o Outputter switch cfg.Format() { case config.AWSCredentialsFormat: @@ -46,5 +47,5 @@ func RenderAWSCredential(cfg *config.Config, oc *oaws.Credential) error { fmt.Fprintf(os.Stderr, "\n") } - return o.Output(cfg, oc) + return o.Output(cfg, oc, ac) } diff --git a/internal/output/process_credentials.go b/internal/output/process_credentials.go index d496721..32c2622 100644 --- a/internal/output/process_credentials.go +++ b/internal/output/process_credentials.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" + "github.com/aws/aws-sdk-go/service/sts" oaws "github.com/okta/okta-aws-cli/internal/aws" "github.com/okta/okta-aws-cli/internal/config" ) @@ -35,12 +36,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 { +func (p *ProcessCredentials) Output(c *config.Config, oc *oaws.Credential, ac *sts.Credentials) error { // 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." - oc.Version = 1 + poc := &oaws.ProcessCredential{ + Credential: *oc, + Expiration: ac.Expiration, + Version: 1, + } - credJSON, err := json.MarshalIndent(oc, "", " ") + credJSON, err := json.MarshalIndent(poc, "", " ") if err != nil { return err } diff --git a/internal/output/process_credentials_test.go b/internal/output/process_credentials_test.go index af4a943..673e122 100644 --- a/internal/output/process_credentials_test.go +++ b/internal/output/process_credentials_test.go @@ -34,7 +34,7 @@ func TestProcessCredentials(t *testing.T) { "SessionToken": "the AWS session token for temporary credentials", "Expiration": "2009-11-10T23:00:00Z" }` - result := aws.Credential{} + result := aws.ProcessCredential{} err := json.Unmarshal([]byte(credsJSON), &result) require.NoError(t, err) require.Equal(t, "an AWS access key", result.AccessKeyID) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index dab36aa..70d96b8 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -31,4 +31,6 @@ const ( XOktaAWSCLIWebOperation = "web" // XOktaAWSCLIM2MOperation m2m op value for the x okta aws cli header XOktaAWSCLIM2MOperation = "m2m" + // PassThroughStringNewLineFMT string formatter to make lint happy + PassThroughStringNewLineFMT = "%s\n" ) diff --git a/internal/webssoauth/webssoauth.go b/internal/webssoauth/webssoauth.go index 7b4cf34..943a0ea 100644 --- a/internal/webssoauth/webssoauth.go +++ b/internal/webssoauth/webssoauth.go @@ -303,12 +303,12 @@ func (w *WebSSOAuthentication) establishTokenWithFedAppID(clientID, fedAppID str return err } - oc, err := w.awsAssumeRoleWithSAML(iar, assertion) + oc, ac, err := w.awsAssumeRoleWithSAML(iar, assertion) if err != nil { return err } - err = output.RenderAWSCredential(w.config, oc) + err = output.RenderAWSCredential(w.config, oc, ac) if err != nil { return err } @@ -325,7 +325,7 @@ func (w *WebSSOAuthentication) establishTokenWithFedAppID(clientID, fedAppID str // awsAssumeRoleWithSAML Get AWS Credentials with an STS Assume Role With SAML AWS // API call. -func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion string) (oc *oaws.Credential, err error) { +func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion string) (oc *oaws.Credential, ac *sts.Credentials, err error) { awsCfg := aws.NewConfig().WithHTTPClient(w.config.HTTPClient()) sess, err := session.NewSession(awsCfg) if err != nil { @@ -347,9 +347,8 @@ func (w *WebSSOAuthentication) awsAssumeRoleWithSAML(iar *idpAndRole, assertion AccessKeyID: *svcResp.Credentials.AccessKeyId, SecretAccessKey: *svcResp.Credentials.SecretAccessKey, SessionToken: *svcResp.Credentials.SessionToken, - Expiration: svcResp.Credentials.Expiration, } - return oc, nil + return oc, svcResp.Credentials, nil } // choiceFriendlyLabelRole returns a friendly choice for pretty printing Role @@ -637,7 +636,7 @@ func (w *WebSSOAuthentication) promptAuthentication(da *okta.DeviceAuthorization buf := bytes.NewBufferString("") qrterminal.GenerateHalfBlock(da.VerificationURIComplete, qrterminal.L, buf) if _, err := buf.Read(qrBuf); err == nil { - qrCode = fmt.Sprintf("%s\n", qrBuf) + qrCode = fmt.Sprintf(utils.PassThroughStringNewLineFMT, qrBuf) } }