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

Add aws-vault export cmd #1135

Merged
merged 1 commit into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 29 additions & 57 deletions cli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
Expand All @@ -23,30 +22,30 @@ import (
)

type ExecCommandInput struct {
ProfileName string
Command string
Args []string
StartEc2Server bool
StartEcsServer bool
Lazy bool
CredentialHelper bool
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
ProfileName string
Command string
Args []string
StartEc2Server bool
StartEcsServer bool
Lazy bool
JSONDeprecated bool
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
}

func (input ExecCommandInput) validate() error {
if input.StartEc2Server && input.StartEcsServer {
return fmt.Errorf("Can't use --server with --ecs-server")
}
if input.StartEc2Server && input.CredentialHelper {
if input.StartEc2Server && input.JSONDeprecated {
return fmt.Errorf("Can't use --server with --json")
}
if input.StartEc2Server && input.NoSession {
return fmt.Errorf("Can't use --server with --no-session")
}
if input.StartEcsServer && input.CredentialHelper {
if input.StartEcsServer && input.JSONDeprecated {
return fmt.Errorf("Can't use --ecs-server with --json")
}
if input.StartEcsServer && input.NoSession {
Expand Down Expand Up @@ -84,7 +83,8 @@ func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) {

cmd.Flag("json", "Output credentials in JSON that can be used by credential_process").
Short('j').
BoolVar(&input.CredentialHelper)
Hidden().
BoolVar(&input.JSONDeprecated)

cmd.Flag("server", "Alias for --ec2-server. Run a EC2 metadata server in the background for credentials").
Short('s').
Expand Down Expand Up @@ -129,7 +129,20 @@ func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) {
return err
}

err = ExecCommand(input, f, keyring)
if input.JSONDeprecated {
exportCommandInput := ExportCommandInput{
ProfileName: input.ProfileName,
Format: "json",
Config: input.Config,
SessionDuration: input.SessionDuration,
NoSession: input.NoSession,
}

err = ExportCommand(exportCommandInput, f, keyring)
} else {
err = ExecCommand(input, f, keyring)
}

app.FatalIfError(err, "exec")
return nil
})
Expand Down Expand Up @@ -171,10 +184,6 @@ func ExecCommand(input ExecCommandInput, f *vault.ConfigFile, keyring keyring.Ke
return execEcsServer(input, config, credsProvider)
}

if input.CredentialHelper {
return execCredentialHelper(input, credsProvider)
}

return execEnvironment(input, config, credsProvider)
}

Expand Down Expand Up @@ -231,43 +240,6 @@ func execEcsServer(input ExecCommandInput, config *vault.Config, credsProvider a
return execCmd(input.Command, input.Args, env)
}

func execCredentialHelper(input ExecCommandInput, credsProvider aws.CredentialsProvider) error {
// AwsCredentialHelperData is metadata for AWS CLI credential process
// See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
type AwsCredentialHelperData struct {
Version int `json:"Version"`
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken,omitempty"`
Expiration string `json:"Expiration,omitempty"`
}

creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

credentialData := AwsCredentialHelperData{
Version: 1,
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
SessionToken: creds.SessionToken,
}

if creds.CanExpire {
credentialData.Expiration = iso8601.Format(creds.Expires)
}

json, err := json.Marshal(&credentialData)
if err != nil {
return fmt.Errorf("Error creating credential json: %w", err)
}

fmt.Print(string(json))

return nil
}

func execEnvironment(input ExecCommandInput, config *vault.Config, credsProvider aws.CredentialsProvider) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
Expand Down
192 changes: 192 additions & 0 deletions cli/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/99designs/aws-vault/v7/iso8601"
"github.com/99designs/aws-vault/v7/vault"
"github.com/99designs/keyring"
"github.com/alecthomas/kingpin"
"github.com/aws/aws-sdk-go-v2/aws"
)

type ExportCommandInput struct {
ProfileName string
Format string
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
}

var (
FormatTypeEnv = "env"
FormatTypeExportEnv = "export-env"
FormatTypeExportJSON = "json"
FormatTypeExportINI = "ini"
)

func ConfigureExportCommand(app *kingpin.Application, a *AwsVault) {
input := ExportCommandInput{}

cmd := app.Command("export", "Export AWS credentials in env, export-env, json or ini format")

cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h").
Short('d').
DurationVar(&input.SessionDuration)

cmd.Flag("no-session", "Skip creating STS session with GetSessionToken").
Short('n').
BoolVar(&input.NoSession)

cmd.Flag("region", "The AWS region").
StringVar(&input.Config.Region)

cmd.Flag("mfa-token", "The MFA token to use").
Short('t').
StringVar(&input.Config.MfaToken)

cmd.Flag("format", fmt.Sprintf("Format to output credentials. Valid values are %s, %s and %s", FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON)).
Default(FormatTypeEnv).
EnumVar(&input.Format, FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI)

cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser").
BoolVar(&input.UseStdout)

cmd.Arg("profile", "Name of the profile").
Required().
HintAction(a.MustGetProfileNames).
StringVar(&input.ProfileName)

cmd.Action(func(c *kingpin.ParseContext) (err error) {
input.Config.MfaPromptMethod = a.PromptDriver
input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration
input.Config.AssumeRoleDuration = input.SessionDuration
input.Config.SSOUseStdout = input.UseStdout

f, err := a.AwsConfigFile()
if err != nil {
return err
}
keyring, err := a.Keyring()
if err != nil {
return err
}

err = ExportCommand(input, f, keyring)
app.FatalIfError(err, "exec")
return nil
})
}

func ExportCommand(input ExportCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error {
if os.Getenv("AWS_VAULT") != "" {
return fmt.Errorf("aws-vault sessions should be nested with care, unset AWS_VAULT to force")
}

vault.UseSession = !input.NoSession

configLoader := vault.ConfigLoader{
File: f,
BaseConfig: input.Config,
ActiveProfile: input.ProfileName,
}
config, err := configLoader.LoadFromProfile(input.ProfileName)
if err != nil {
return fmt.Errorf("Error loading config: %w", err)
}

ckr := &vault.CredentialKeyring{Keyring: keyring}
credsProvider, err := vault.NewTempCredentialsProvider(config, ckr)
if err != nil {
return fmt.Errorf("Error getting temporary credentials: %w", err)
}

if input.Format == FormatTypeExportJSON {
return printJSON(input, credsProvider)
} else if input.Format == FormatTypeExportINI {
return printINI(input, credsProvider)
} else if input.Format == FormatTypeExportEnv {
return printEnv(input, credsProvider, "export ")
} else {
return printEnv(input, credsProvider, "")
}
}

func printJSON(input ExportCommandInput, credsProvider aws.CredentialsProvider) error {
// AwsCredentialHelperData is metadata for AWS CLI credential process
// See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
type AwsCredentialHelperData struct {
Version int `json:"Version"`
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken,omitempty"`
Expiration string `json:"Expiration,omitempty"`
}

creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

credentialData := AwsCredentialHelperData{
Version: 1,
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
SessionToken: creds.SessionToken,
}

if creds.CanExpire {
credentialData.Expiration = iso8601.Format(creds.Expires)
}

json, err := json.MarshalIndent(&credentialData, "", " ")
if err != nil {
return fmt.Errorf("Error creating credential json: %w", err)
}

fmt.Print(string(json) + "\n")

return nil
}

func printINI(input ExportCommandInput, credsProvider aws.CredentialsProvider) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

fmt.Printf("[%s]\n", input.ProfileName)
fmt.Printf("aws_access_key_id=%s\n", creds.AccessKeyID)
fmt.Printf("aws_secret_access_key=%s\n", creds.SecretAccessKey)

if creds.SessionToken != "" {
fmt.Printf("aws_session_token=%s\n", creds.SessionToken)
}
if creds.CanExpire {
fmt.Printf(";aws_credential_expiration=%s\n", iso8601.Format(creds.Expires))
}
return nil
}

func printEnv(input ExportCommandInput, credsProvider aws.CredentialsProvider, prefix string) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

fmt.Printf("%sAWS_ACCESS_KEY_ID=%s\n", prefix, creds.AccessKeyID)
fmt.Printf("%sAWS_SECRET_ACCESS_KEY=%s\n", prefix, creds.SecretAccessKey)

if creds.SessionToken != "" {
fmt.Printf("%sAWS_SESSION_TOKEN=%s\n", prefix, creds.SessionToken)
}
if creds.CanExpire {
fmt.Printf("%sAWS_CREDENTIAL_EXPIRATION=%s\n", prefix, iso8601.Format(creds.Expires))
}
return nil
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func main() {
cli.ConfigureListCommand(app, a)
cli.ConfigureRotateCommand(app, a)
cli.ConfigureExecCommand(app, a)
cli.ConfigureExportCommand(app, a)
cli.ConfigureClearCommand(app, a)
cli.ConfigureRemoveCommand(app, a)
cli.ConfigureLoginCommand(app, a)
Expand Down