Skip to content

Commit

Permalink
Merge pull request #144 from okta/m2m_exec_from_command
Browse files Browse the repository at this point in the history
`--exec` subcommand
  • Loading branch information
MikeMondragon-okta authored Oct 5, 2023
2 parents 1269a4b + 36c0373 commit f6ddcaa
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 65 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
19 changes: 12 additions & 7 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/aws/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -174,6 +180,7 @@ type Config struct {
customScope string
debug bool
debugAPICalls bool
exec bool
expiryAWSVariables bool
fedAppID string
format string
Expand Down Expand Up @@ -201,6 +208,7 @@ type Attributes struct {
CustomScope string
Debug bool
DebugAPICalls bool
Exec bool
ExpiryAWSVariables bool
FedAppID string
Format string
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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"
"github.com/okta/okta-aws-cli/internal/utils"
)

// 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, utils.PassThroughStringNewLineFMT, ee.Stderr)
}
if err != nil {
return err
}

fmt.Printf("%s", string(out))
return nil
}
Loading

0 comments on commit f6ddcaa

Please sign in to comment.