diff --git a/.travis.yml b/.travis.yml index 377192086..fdef53859 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ language: go -install: ./ci/install.sh +install: ./scripts/ci_install.sh go: - 1.7 + - 1.8 + - 1.9 os: - linux diff --git a/add.go b/cli/add.go similarity index 68% rename from add.go rename to cli/add.go index c8552571d..57be5baee 100644 --- a/add.go +++ b/cli/add.go @@ -1,10 +1,11 @@ -package main +package cli import ( "fmt" "os" "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go/aws/credentials" "gopkg.in/alecthomas/kingpin.v2" @@ -16,6 +17,24 @@ type AddCommandInput struct { FromEnv bool } +func ConfigureAddCommand(app *kingpin.Application) { + input := AddCommandInput{} + + cmd := app.Command("add", "Adds credentials, prompts if none provided") + cmd.Arg("profile", "Name of the profile"). + Required(). + StringVar(&input.Profile) + + cmd.Flag("env", "Read the credentials from the environment"). + BoolVar(&input.FromEnv) + + cmd.Action(func(c *kingpin.ParseContext) error { + input.Keyring = keyringImpl + AddCommand(app, input) + return nil + }) +} + func AddCommand(app *kingpin.Application, input AddCommandInput) { var accessKeyId, secretKey string @@ -41,7 +60,7 @@ func AddCommand(app *kingpin.Application, input AddCommandInput) { } creds := credentials.Value{AccessKeyID: accessKeyId, SecretAccessKey: secretKey} - provider := &KeyringProvider{Keyring: input.Keyring, Profile: input.Profile} + provider := &vault.KeyringProvider{Keyring: input.Keyring, Profile: input.Profile} if err := provider.Store(creds); err != nil { app.Fatalf(err.Error()) @@ -56,7 +75,7 @@ func AddCommand(app *kingpin.Application, input AddCommandInput) { return } - sessions, err := NewKeyringSessions(input.Keyring, profiles) + sessions, err := vault.NewKeyringSessions(input.Keyring, profiles) if err != nil { app.Fatalf(err.Error()) return diff --git a/add_test.go b/cli/add_test.go similarity index 66% rename from add_test.go rename to cli/add_test.go index 86253f6d1..c6c7904a4 100644 --- a/add_test.go +++ b/cli/add_test.go @@ -1,6 +1,10 @@ -package main +package cli -import "os" +import ( + "os" + + kingpin "gopkg.in/alecthomas/kingpin.v2" +) func ExampleAddCommand() { os.Setenv("AWS_ACCESS_KEY_ID", "llamas") @@ -13,7 +17,11 @@ func ExampleAddCommand() { defer os.Unsetenv("AWS_VAULT_BACKEND") defer os.Unsetenv("AWS_VAULT_FILE_PASSPHRASE") - run([]string{"add", "--env", "foo"}, os.Exit) + app := kingpin.New(`aws-vault`, ``) + ConfigureGlobals(app) + ConfigureAddCommand(app) + kingpin.MustParse(app.Parse([]string{"add", "--env", "foo"})) + // Output: // Added credentials to profile "foo" in vault } diff --git a/exec.go b/cli/exec.go similarity index 66% rename from exec.go rename to cli/exec.go index 752a40c81..8d4577957 100644 --- a/exec.go +++ b/cli/exec.go @@ -1,14 +1,17 @@ -package main +package cli import ( "log" "os" "os/exec" + "os/signal" "strings" "syscall" "time" "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/server" + "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" "gopkg.in/alecthomas/kingpin.v2" ) @@ -27,6 +30,54 @@ type ExecCommandInput struct { NoSession bool } +func ConfigureExecCommand(app *kingpin.Application) { + input := ExecCommandInput{} + + cmd := app.Command("exec", "Executes a command with AWS credentials in the environment") + cmd.Flag("no-session", "Use root credentials, no session created"). + Short('n'). + BoolVar(&input.NoSession) + + cmd.Flag("session-ttl", "Expiration time for aws session"). + Default("4h"). + OverrideDefaultFromEnvar("AWS_SESSION_TTL"). + Short('t'). + DurationVar(&input.Duration) + + cmd.Flag("assume-role-ttl", "Expiration time for aws assumed role"). + Default("15m"). + OverrideDefaultFromEnvar("AWS_ASSUME_ROLE_TTL"). + DurationVar(&input.RoleDuration) + + cmd.Flag("mfa-token", "The mfa token to use"). + Short('m'). + StringVar(&input.MfaToken) + + cmd.Flag("server", "Run the server in the background for credentials"). + Short('s'). + BoolVar(&input.StartServer) + + cmd.Arg("profile", "Name of the profile"). + Required(). + StringVar(&input.Profile) + + cmd.Arg("cmd", "Command to execute"). + Default(os.Getenv("SHELL")). + StringVar(&input.Command) + + cmd.Arg("args", "Command arguments"). + StringsVar(&input.Args) + + cmd.Action(func(c *kingpin.ParseContext) error { + input.Keyring = keyringImpl + input.MfaPrompt = prompt.Method(GlobalFlags.PromptDriver) + input.Signals = make(chan os.Signal) + signal.Notify(input.Signals, os.Interrupt, os.Kill) + ExecCommand(app, input) + return nil + }) +} + func ExecCommand(app *kingpin.Application, input ExecCommandInput) { if os.Getenv("AWS_VAULT") != "" { app.Fatalf("aws-vault sessions should be nested with care, unset $AWS_VAULT to force") @@ -46,7 +97,7 @@ func ExecCommand(app *kingpin.Application, input ExecCommandInput) { return } - creds, err := NewVaultCredentials(input.Keyring, input.Profile, VaultOptions{ + creds, err := vault.NewVaultCredentials(input.Keyring, input.Profile, vault.VaultOptions{ SessionDuration: input.Duration, AssumeRoleDuration: input.RoleDuration, MfaToken: input.MfaToken, @@ -60,11 +111,11 @@ func ExecCommand(app *kingpin.Application, input ExecCommandInput) { val, err := creds.Get() if err != nil { - app.Fatalf(formatCredentialError(input.Profile, profiles, err)) + app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) } if input.StartServer { - if err := startCredentialsServer(creds); err != nil { + if err := server.StartCredentialsServer(creds); err != nil { app.Fatalf("Failed to start credential server: %v", err) } else { setEnv = false diff --git a/cli/exec_test.go b/cli/exec_test.go new file mode 100644 index 000000000..8d395443e --- /dev/null +++ b/cli/exec_test.go @@ -0,0 +1,25 @@ +package cli + +import ( + kingpin "gopkg.in/alecthomas/kingpin.v2" + + "github.com/99designs/aws-vault/vault" + "github.com/99designs/keyring" +) + +func ExampleExecCommand() { + awsConfigFile = &vault.FileConfig{} + keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ + {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, + }) + + app := kingpin.New(`aws-vault`, ``) + ConfigureGlobals(app) + ConfigureExecCommand(app) + kingpin.MustParse(app.Parse([]string{ + "--debug", "exec", "--no-session", "llamas", "--", "sh", "-c", "echo $AWS_ACCESS_KEY_ID", + })) + + // Output: + // ABC +} diff --git a/cli/global.go b/cli/global.go new file mode 100644 index 000000000..0a62bfdb6 --- /dev/null +++ b/cli/global.go @@ -0,0 +1,58 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "log" + + "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/vault" + "github.com/99designs/keyring" + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +const ( + KeyringName = "aws-vault" +) + +var ( + keyringImpl keyring.Keyring + awsConfigFile vault.Config + promptsAvailable = prompt.Available() + backendsAvailable = keyring.SupportedBackends() +) + +var GlobalFlags struct { + Debug bool + Backend string + PromptDriver string +} + +func ConfigureGlobals(app *kingpin.Application) { + app.Flag("debug", "Show debugging output"). + BoolVar(&GlobalFlags.Debug) + + app.Flag("backend", fmt.Sprintf("Secret backend to use %v", backendsAvailable)). + Default(keyring.DefaultBackend). + OverrideDefaultFromEnvar("AWS_VAULT_BACKEND"). + EnumVar(&GlobalFlags.Backend, backendsAvailable...) + + app.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", promptsAvailable)). + Default("terminal"). + OverrideDefaultFromEnvar("AWS_VAULT_PROMPT"). + EnumVar(&GlobalFlags.PromptDriver, promptsAvailable...) + + app.PreAction(func(c *kingpin.ParseContext) (err error) { + if !GlobalFlags.Debug { + log.SetOutput(ioutil.Discard) + } + if keyringImpl == nil { + keyringImpl, err = keyring.Open(KeyringName, GlobalFlags.Backend) + } + if awsConfigFile == nil { + awsConfigFile, err = vault.NewConfigFromEnv() + } + return err + }) + +} diff --git a/login.go b/cli/login.go similarity index 80% rename from login.go rename to cli/login.go index ff96a5d51..bc8cf9117 100644 --- a/login.go +++ b/cli/login.go @@ -1,4 +1,4 @@ -package main +package cli import ( "encoding/json" @@ -10,6 +10,7 @@ import ( "time" "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -32,6 +33,40 @@ type LoginCommandInput struct { AssumeRoleDuration time.Duration } +func ConfigureLoginCommand(app *kingpin.Application) { + input := LoginCommandInput{} + + cmd := app.Command("login", "Generate a login link for the AWS Console") + cmd.Arg("profile", "Name of the profile"). + Required(). + StringVar(&input.Profile) + + cmd.Flag("mfa-token", "The mfa token to use"). + Short('t'). + StringVar(&input.MfaToken) + + cmd.Flag("federation-token-ttl", "Expiration time for aws console session"). + Default("12h"). + OverrideDefaultFromEnvar("AWS_FEDERATION_TOKEN_TTL"). + Short('f'). + DurationVar(&input.FederationTokenDuration) + + cmd.Flag("assume-role-ttl", "Expiration time for aws assumed role"). + Default("15m"). + DurationVar(&input.AssumeRoleDuration) + + cmd.Flag("stdout", "Print login URL to stdout instead of opening in default browser"). + Short('s'). + BoolVar(&input.UseStdout) + + cmd.Action(func(c *kingpin.ParseContext) error { + input.MfaPrompt = prompt.Method(GlobalFlags.PromptDriver) + input.Keyring = keyringImpl + LoginCommand(app, input) + return nil + }) +} + func LoginCommand(app *kingpin.Application, input LoginCommandInput) { if input.FederationTokenDuration > (time.Hour * 12) { app.Fatalf("Maximum federation token duration is 12 hours") @@ -44,7 +79,7 @@ func LoginCommand(app *kingpin.Application, input LoginCommandInput) { return } - provider, err := NewVaultProvider(input.Keyring, input.Profile, VaultOptions{ + provider, err := vault.NewVaultProvider(input.Keyring, input.Profile, vault.VaultOptions{ AssumeRoleDuration: input.AssumeRoleDuration, MfaToken: input.MfaToken, MfaPrompt: input.MfaPrompt, @@ -59,7 +94,7 @@ func LoginCommand(app *kingpin.Application, input LoginCommandInput) { creds := credentials.NewCredentials(provider) val, err := creds.Get() if err != nil { - app.Fatalf(formatCredentialError(input.Profile, profiles, err)) + app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) } var isFederated bool diff --git a/ls.go b/cli/ls.go similarity index 59% rename from ls.go rename to cli/ls.go index 95fcaa9c0..1ec4947c9 100644 --- a/ls.go +++ b/cli/ls.go @@ -1,4 +1,4 @@ -package main +package cli import ( "fmt" @@ -11,6 +11,18 @@ type LsCommandInput struct { Keyring keyring.Keyring } +func ConfigureListCommand(app *kingpin.Application) { + input := LsCommandInput{} + + cmd := app.Command("list", "List all credentials and sessions") + cmd.Alias("ls") + cmd.Action(func(c *kingpin.ParseContext) error { + input.Keyring = keyringImpl + LsCommand(app, input) + return nil + }) +} + func LsCommand(app *kingpin.Application, input LsCommandInput) { accounts, err := input.Keyring.Keys() if err != nil { diff --git a/ls_test.go b/cli/ls_test.go similarity index 55% rename from ls_test.go rename to cli/ls_test.go index 13443ad0c..a9500d1c8 100644 --- a/ls_test.go +++ b/cli/ls_test.go @@ -1,7 +1,7 @@ -package main +package cli import ( - "os" + kingpin "gopkg.in/alecthomas/kingpin.v2" "github.com/99designs/keyring" ) @@ -11,7 +11,13 @@ func ExampleListCommand() { {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, }) - run([]string{"list"}, os.Exit) + app := kingpin.New(`aws-vault`, ``) + ConfigureGlobals(app) + ConfigureListCommand(app) + kingpin.MustParse(app.Parse([]string{ + "list", + })) + // Output: // llamas } diff --git a/rm.go b/cli/rm.go similarity index 57% rename from rm.go rename to cli/rm.go index aabec9980..b209095e2 100644 --- a/rm.go +++ b/cli/rm.go @@ -1,9 +1,10 @@ -package main +package cli import ( "fmt" "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" "gopkg.in/alecthomas/kingpin.v2" ) @@ -14,9 +15,30 @@ type RemoveCommandInput struct { SessionsOnly bool } +func ConfigureRemoveCommand(app *kingpin.Application) { + input := RemoveCommandInput{} + + cmd := app.Command("remove", "Removes credentials, including sessions") + cmd.Alias("rm") + + cmd.Arg("profile", "Name of the profile"). + Required(). + StringVar(&input.Profile) + + cmd.Flag("sessions-only", "Only remove sessions, leave credentials intact"). + Short('s'). + BoolVar(&input.SessionsOnly) + + cmd.Action(func(c *kingpin.ParseContext) error { + input.Keyring = keyringImpl + RemoveCommand(app, input) + return nil + }) +} + func RemoveCommand(app *kingpin.Application, input RemoveCommandInput) { if !input.SessionsOnly { - provider := &KeyringProvider{Keyring: input.Keyring, Profile: input.Profile} + provider := &vault.KeyringProvider{Keyring: input.Keyring, Profile: input.Profile} r, err := prompt.TerminalPrompt(fmt.Sprintf("Delete credentials for profile %q? (Y|n)", input.Profile)) if err != nil { app.Fatalf(err.Error()) @@ -38,7 +60,7 @@ func RemoveCommand(app *kingpin.Application, input RemoveCommandInput) { return } - sessions, err := NewKeyringSessions(input.Keyring, profiles) + sessions, err := vault.NewKeyringSessions(input.Keyring, profiles) if err != nil { app.Fatalf(err.Error()) return diff --git a/rotate.go b/cli/rotate.go similarity index 76% rename from rotate.go rename to cli/rotate.go index 906f17ea6..c4dbababa 100644 --- a/rotate.go +++ b/cli/rotate.go @@ -1,9 +1,10 @@ -package main +package cli import ( "log" "github.com/99designs/aws-vault/prompt" + "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -19,6 +20,26 @@ type RotateCommandInput struct { MfaPrompt prompt.PromptFunc } +func ConfigureRotateCommand(app *kingpin.Application) { + input := RotateCommandInput{} + + cmd := app.Command("rotate", "Rotates credentials") + cmd.Arg("profile", "Name of the profile"). + Required(). + StringVar(&input.Profile) + + cmd.Flag("mfa-token", "The mfa token to use"). + Short('t'). + StringVar(&input.MfaToken) + + cmd.Action(func(c *kingpin.ParseContext) error { + input.MfaPrompt = prompt.Method(GlobalFlags.PromptDriver) + input.Keyring = keyringImpl + RotateCommand(app, input) + return nil + }) +} + func RotateCommand(app *kingpin.Application, input RotateCommandInput) { var err error @@ -28,9 +49,9 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { return } - provider := &KeyringProvider{ + provider := &vault.KeyringProvider{ Keyring: input.Keyring, - Profile: sourceProfile(input.Profile, profiles), + Profile: profiles.SourceProfile(input.Profile), } oldMasterCreds, err := provider.Retrieve() @@ -56,7 +77,7 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { // We need to use a session as some credentials will requiring assuming a role to // get permission to create creds - oldSessionCreds, err := NewVaultCredentials(input.Keyring, input.Profile, VaultOptions{ + oldSessionCreds, err := vault.NewVaultCredentials(input.Keyring, input.Profile, vault.VaultOptions{ MfaToken: input.MfaToken, MfaPrompt: input.MfaPrompt, Profiles: profiles, @@ -72,7 +93,7 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { oldSessionVal, err := oldSessionCreds.Get() if err != nil { - app.Fatalf(formatCredentialError(input.Profile, profiles, err)) + app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) return } @@ -102,7 +123,7 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { return } - sessions, err := NewKeyringSessions(input.Keyring, profiles) + sessions, err := vault.NewKeyringSessions(input.Keyring, profiles) if err != nil { app.Fatalf(err.Error()) return @@ -114,7 +135,7 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { log.Println("Using new credentials to delete the old new access key") - newSessionCreds, err := NewVaultCredentials(input.Keyring, input.Profile, VaultOptions{ + newSessionCreds, err := vault.NewVaultCredentials(input.Keyring, input.Profile, vault.VaultOptions{ MfaToken: input.MfaToken, MfaPrompt: input.MfaPrompt, Profiles: profiles, @@ -128,7 +149,7 @@ func RotateCommand(app *kingpin.Application, input RotateCommandInput) { newVal, err := newSessionCreds.Get() if err != nil { - app.Fatalf(formatCredentialError(input.Profile, profiles, err)) + app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) return } diff --git a/cli/server.go b/cli/server.go new file mode 100644 index 000000000..60a735b41 --- /dev/null +++ b/cli/server.go @@ -0,0 +1,25 @@ +package cli + +import ( + "github.com/99designs/aws-vault/server" + kingpin "gopkg.in/alecthomas/kingpin.v2" +) + +type ServerCommandInput struct { +} + +func ConfigureServerCommand(app *kingpin.Application) { + input := ServerCommandInput{} + + cmd := app.Command("server", "Run an ec2 instance role server locally") + cmd.Action(func(c *kingpin.ParseContext) error { + ServerCommand(app, input) + return nil + }) +} + +func ServerCommand(app *kingpin.Application, input ServerCommandInput) { + if err := server.StartMetadataServer(); err != nil { + app.Fatalf("Server failed: %v", err) + } +} diff --git a/exec_test.go b/exec_test.go deleted file mode 100644 index 33eae2752..000000000 --- a/exec_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "os" - - "github.com/99designs/keyring" -) - -func ExampleExecCommand() { - awsConfigFile = &fileConfig{} - keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ - {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, - }) - - run([]string{"--debug", "exec", "--no-session", "llamas", "--", "sh", "-c", "echo $AWS_ACCESS_KEY_ID"}, os.Exit) - // Output: - // ABC -} diff --git a/main.go b/main.go index 83caf262f..59ea04c8a 100644 --- a/main.go +++ b/main.go @@ -1,247 +1,37 @@ package main import ( - "fmt" - "io/ioutil" - "log" "os" - "os/signal" - "github.com/99designs/aws-vault/prompt" - "github.com/99designs/keyring" + "github.com/99designs/aws-vault/cli" "gopkg.in/alecthomas/kingpin.v2" ) -const ( - KeyringName = "aws-vault" -) - -var ( - Version string = "dev" - - keyringImpl keyring.Keyring - awsConfigFile config - promptsAvailable = prompt.Available() - backendsAvailable = keyring.SupportedBackends() -) - -type globalFlags struct { - Debug bool - Backend string - PromptDriver string -} - -func configureAddCommand(app *kingpin.Application, g *globalFlags) { - input := AddCommandInput{} - - cmd := app.Command("add", "Adds credentials, prompts if none provided") - cmd.Arg("profile", "Name of the profile"). - Required(). - StringVar(&input.Profile) - - cmd.Flag("env", "Read the credentials from the environment"). - BoolVar(&input.FromEnv) - - cmd.Action(func(c *kingpin.ParseContext) error { - input.Keyring = keyringImpl - AddCommand(app, input) - return nil - }) -} - -func configureListCommand(app *kingpin.Application, g *globalFlags) { - input := LsCommandInput{} - - cmd := app.Command("list", "List all credentials and sessions") - cmd.Alias("ls") - cmd.Action(func(c *kingpin.ParseContext) error { - input.Keyring = keyringImpl - LsCommand(app, input) - return nil - }) -} - -func configureRotateCommand(app *kingpin.Application, g *globalFlags) { - input := RotateCommandInput{} - - cmd := app.Command("rotate", "Rotates credentials") - cmd.Arg("profile", "Name of the profile"). - Required(). - StringVar(&input.Profile) - - cmd.Flag("mfa-token", "The mfa token to use"). - Short('t'). - StringVar(&input.MfaToken) - - cmd.Action(func(c *kingpin.ParseContext) error { - input.MfaPrompt = prompt.Method(g.PromptDriver) - input.Keyring = keyringImpl - RotateCommand(app, input) - return nil - }) -} - -func configureExecCommand(app *kingpin.Application, g *globalFlags) { - input := ExecCommandInput{} - - cmd := app.Command("exec", "Executes a command with AWS credentials in the environment") - cmd.Flag("no-session", "Use root credentials, no session created"). - Short('n'). - BoolVar(&input.NoSession) - - cmd.Flag("session-ttl", "Expiration time for aws session"). - Default("4h"). - OverrideDefaultFromEnvar("AWS_SESSION_TTL"). - Short('t'). - DurationVar(&input.Duration) - - cmd.Flag("assume-role-ttl", "Expiration time for aws assumed role"). - Default("15m"). - OverrideDefaultFromEnvar("AWS_ASSUME_ROLE_TTL"). - DurationVar(&input.RoleDuration) - - cmd.Flag("mfa-token", "The mfa token to use"). - Short('m'). - StringVar(&input.MfaToken) - - cmd.Flag("server", "Run the server in the background for credentials"). - Short('s'). - BoolVar(&input.StartServer) - - cmd.Arg("profile", "Name of the profile"). - Required(). - StringVar(&input.Profile) - - cmd.Arg("cmd", "Command to execute"). - Default(os.Getenv("SHELL")). - StringVar(&input.Command) - - cmd.Arg("args", "Command arguments"). - StringsVar(&input.Args) - - cmd.Action(func(c *kingpin.ParseContext) error { - input.Keyring = keyringImpl - input.MfaPrompt = prompt.Method(g.PromptDriver) - input.Signals = make(chan os.Signal) - signal.Notify(input.Signals, os.Interrupt, os.Kill) - ExecCommand(app, input) - return nil - }) -} - -func configureRemoveCommand(app *kingpin.Application, g *globalFlags) { - input := RemoveCommandInput{} - - cmd := app.Command("remove", "Removes credentials, including sessions") - cmd.Alias("rm") - - cmd.Arg("profile", "Name of the profile"). - Required(). - StringVar(&input.Profile) - - cmd.Flag("sessions-only", "Only remove sessions, leave credentials intact"). - Short('s'). - BoolVar(&input.SessionsOnly) - - cmd.Action(func(c *kingpin.ParseContext) error { - input.Keyring = keyringImpl - RemoveCommand(app, input) - return nil - }) -} - -func configureLoginCommand(app *kingpin.Application, g *globalFlags) { - input := LoginCommandInput{} - - cmd := app.Command("login", "Generate a login link for the AWS Console") - cmd.Arg("profile", "Name of the profile"). - Required(). - StringVar(&input.Profile) - - cmd.Flag("mfa-token", "The mfa token to use"). - Short('t'). - StringVar(&input.MfaToken) - - cmd.Flag("federation-token-ttl", "Expiration time for aws console session"). - Default("12h"). - OverrideDefaultFromEnvar("AWS_FEDERATION_TOKEN_TTL"). - Short('f'). - DurationVar(&input.FederationTokenDuration) - - cmd.Flag("assume-role-ttl", "Expiration time for aws assumed role"). - Default("15m"). - DurationVar(&input.AssumeRoleDuration) - - cmd.Flag("stdout", "Print login URL to stdout instead of opening in default browser"). - Short('s'). - BoolVar(&input.UseStdout) - - cmd.Action(func(c *kingpin.ParseContext) error { - input.MfaPrompt = prompt.Method(g.PromptDriver) - input.Keyring = keyringImpl - LoginCommand(app, input) - return nil - }) -} +// Version is provided at compile time +var Version = "dev" -func configureServerCommand(app *kingpin.Application, g *globalFlags) { - input := ServerCommandInput{} - - cmd := app.Command("server", "Run an ec2 instance role server locally") - cmd.Action(func(c *kingpin.ParseContext) error { - ServerCommand(app, input) - return nil - }) +func main() { + run(os.Args[1:], os.Exit) } -func run(args []string, onTerminate func(int)) { - app := kingpin.New("aws-vault", - `A vault for securely storing and accessing AWS credentials in development environments.`) - - globals := &globalFlags{} +func run(args []string, exit func(int)) { + app := kingpin.New( + `aws-vault`, + `A vault for securely storing and accessing AWS credentials in development environments.`, + ) app.Writer(os.Stdout) app.Version(Version) - - app.Flag("debug", "Show debugging output"). - BoolVar(&globals.Debug) - - app.Flag("backend", fmt.Sprintf("Secret backend to use %v", backendsAvailable)). - Default(keyring.DefaultBackend). - OverrideDefaultFromEnvar("AWS_VAULT_BACKEND"). - EnumVar(&globals.Backend, backendsAvailable...) - - app.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", promptsAvailable)). - Default("terminal"). - OverrideDefaultFromEnvar("AWS_VAULT_PROMPT"). - EnumVar(&globals.PromptDriver, promptsAvailable...) - - app.PreAction(func(c *kingpin.ParseContext) (err error) { - if !globals.Debug { - log.SetOutput(ioutil.Discard) - } - if keyringImpl == nil { - keyringImpl, err = keyring.Open(KeyringName, globals.Backend) - } - if awsConfigFile == nil { - awsConfigFile, err = newConfigFromEnv() - } - return err - }) - - configureAddCommand(app, globals) - configureListCommand(app, globals) - configureRotateCommand(app, globals) - configureExecCommand(app, globals) - configureRemoveCommand(app, globals) - configureLoginCommand(app, globals) - configureServerCommand(app, globals) - - if _, err := app.Parse(args); err != nil { - app.Fatalf("%v", err) - } -} - -func main() { - run(os.Args[1:], os.Exit) + app.Terminate(exit) + + cli.ConfigureGlobals(app) + cli.ConfigureAddCommand(app) + cli.ConfigureListCommand(app) + cli.ConfigureRotateCommand(app) + cli.ConfigureExecCommand(app) + cli.ConfigureRemoveCommand(app) + cli.ConfigureLoginCommand(app) + cli.ConfigureServerCommand(app) + + kingpin.MustParse(app.Parse(args)) } diff --git a/ci/install.sh b/scripts/ci_install.sh similarity index 100% rename from ci/install.sh rename to scripts/ci_install.sh diff --git a/alias_darwin.go b/server/alias_darwin.go similarity index 91% rename from alias_darwin.go rename to server/alias_darwin.go index df7716434..e87cd14c5 100644 --- a/alias_darwin.go +++ b/server/alias_darwin.go @@ -1,6 +1,6 @@ // build: darwin -package main +package server import "os/exec" diff --git a/alias_linux.go b/server/alias_linux.go similarity index 92% rename from alias_linux.go rename to server/alias_linux.go index 662824191..5f19e0178 100644 --- a/alias_linux.go +++ b/server/alias_linux.go @@ -1,6 +1,6 @@ // build: linux -package main +package server import "os/exec" diff --git a/alias_windows.go b/server/alias_windows.go similarity index 91% rename from alias_windows.go rename to server/alias_windows.go index fe29b1cd7..30ac72cd5 100644 --- a/alias_windows.go +++ b/server/alias_windows.go @@ -1,6 +1,6 @@ // build: windows -package main +package server import "fmt" diff --git a/server.go b/server/server.go similarity index 86% rename from server.go rename to server/server.go index 686b84462..8bd0c8393 100644 --- a/server.go +++ b/server/server.go @@ -1,4 +1,4 @@ -package main +package server import ( "encoding/json" @@ -11,7 +11,7 @@ import ( "os/exec" "time" - "gopkg.in/alecthomas/kingpin.v2" + "github.com/99designs/aws-vault/vault" ) const ( @@ -21,14 +21,9 @@ const ( localServerBind = "127.0.0.1:9099" ) -type ServerCommandInput struct { -} - -func ServerCommand(app *kingpin.Application, input ServerCommandInput) { - if output, err := installNetworkAlias(); err != nil { - app.Errorf(string(output)) - app.Fatalf(err.Error()) - return +func StartMetadataServer() error { + if _, err := installNetworkAlias(); err != nil { + return err } router := http.NewServeMux() @@ -37,12 +32,11 @@ func ServerCommand(app *kingpin.Application, input ServerCommandInput) { l, err := net.Listen("tcp", metadataBind) if err != nil { - app.Fatalf(err.Error()) - return + return err } log.Printf("Local instance role server running on %s", l.Addr()) - app.Errorf(http.Serve(l, router).Error()) + return http.Serve(l, router) } type metadataHandler struct { @@ -74,7 +68,7 @@ func checkServerRunning(bind string) bool { return err == nil } -func startCredentialsServer(creds *VaultCredentials) error { +func StartCredentialsServer(creds *vault.VaultCredentials) error { if !checkServerRunning(metadataBind) { log.Printf("Starting `aws-vault server` as root in the background") cmd := exec.Command("sudo", "-b", os.Args[0], "server") diff --git a/config.go b/vault/config.go similarity index 66% rename from config.go rename to vault/config.go index fba199200..079b550b2 100644 --- a/config.go +++ b/vault/config.go @@ -1,4 +1,4 @@ -package main +package vault import ( "fmt" @@ -12,17 +12,27 @@ import ( "github.com/vaughan0/go-ini" ) -type profiles map[string]map[string]string +type Profiles map[string]map[string]string -type config interface { - Parse() (profiles, error) +// SourceProfile returns either the defined source_profile or profileKey if none exists +func (p Profiles) SourceProfile(profileKey string) string { + if conf, ok := p[profileKey]; ok { + if source := conf["source_profile"]; source != "" { + return source + } + } + return profileKey +} + +type Config interface { + Parse() (Profiles, error) } -type fileConfig struct { +type FileConfig struct { file string } -func newConfigFromEnv() (config, error) { +func NewConfigFromEnv() (Config, error) { file := os.Getenv("AWS_CONFIG_FILE") if file == "" { home, err := homedir.Dir() @@ -34,10 +44,10 @@ func newConfigFromEnv() (config, error) { file = "" } } - return &fileConfig{file: file}, nil + return &FileConfig{file: file}, nil } -func (c *fileConfig) Parse() (profiles, error) { +func (c *FileConfig) Parse() (Profiles, error) { if c.file == "" { return nil, nil } @@ -48,7 +58,7 @@ func (c *fileConfig) Parse() (profiles, error) { return nil, fmt.Errorf("Error parsing config file %q: %v", c.file, err) } - profiles := profiles{} + profiles := Profiles{} for sectionName, section := range f { profiles[strings.TrimPrefix(sectionName, "profile ")] = section @@ -57,23 +67,13 @@ func (c *fileConfig) Parse() (profiles, error) { return profiles, nil } -// sourceProfile returns either the defined source_profile or p if none exists -func sourceProfile(p string, from profiles) string { - if conf, ok := from[p]; ok { - if source := conf["source_profile"]; source != "" { - return source - } - } - return p -} - -func formatCredentialError(p string, from profiles, err error) string { - source := sourceProfile(p, from) - sourceDescr := p +func FormatCredentialError(profileKey string, from Profiles, err error) string { + source := from.SourceProfile(profileKey) + sourceDescr := profileKey // add custom formatting for source_profile - if source != p { - sourceDescr = fmt.Sprintf("%s (source profile for %s)", source, p) + if source != profileKey { + sourceDescr = fmt.Sprintf("%s (source profile for %s)", source, profileKey) } if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { diff --git a/provider.go b/vault/provider.go similarity index 97% rename from provider.go rename to vault/provider.go index c24885f27..5c3417d56 100644 --- a/provider.go +++ b/vault/provider.go @@ -1,4 +1,4 @@ -package main +package vault import ( "encoding/json" @@ -32,7 +32,7 @@ type VaultOptions struct { MfaToken string MfaPrompt prompt.PromptFunc NoSession bool - Profiles profiles + Profiles Profiles MasterCreds *credentials.Value } @@ -68,7 +68,7 @@ type VaultProvider struct { expires time.Time keyring keyring.Keyring sessions *KeyringSessions - profiles profiles + profiles Profiles creds map[string]credentials.Value } @@ -175,7 +175,7 @@ func (p *VaultProvider) getMasterCreds() (credentials.Value, error) { return *p.MasterCreds, nil } - source := sourceProfile(p.profile, p.profiles) + source := p.profiles.SourceProfile(p.profile) val, ok := p.creds[source] if !ok { @@ -217,7 +217,7 @@ func (p *VaultProvider) getSessionToken(creds *credentials.Value) (sts.Credentia }), })) - log.Printf("Getting new session token for profile %s", sourceProfile(p.profile, p.profiles)) + log.Printf("Getting new session token for profile %s", p.profiles.SourceProfile(p.profile)) resp, err := client.GetSessionToken(params) if err != nil { return sts.Credentials{}, err diff --git a/sessions.go b/vault/sessions.go similarity index 88% rename from sessions.go rename to vault/sessions.go index e611016a7..54f02fb0d 100644 --- a/sessions.go +++ b/vault/sessions.go @@ -1,4 +1,4 @@ -package main +package vault import ( "crypto/md5" @@ -16,10 +16,10 @@ import ( type KeyringSessions struct { Keyring keyring.Keyring - Profiles profiles + Profiles Profiles } -func NewKeyringSessions(k keyring.Keyring, p profiles) (*KeyringSessions, error) { +func NewKeyringSessions(k keyring.Keyring, p Profiles) (*KeyringSessions, error) { return &KeyringSessions{ Keyring: k, Profiles: p, @@ -27,7 +27,7 @@ func NewKeyringSessions(k keyring.Keyring, p profiles) (*KeyringSessions, error) } func (s *KeyringSessions) key(profile string, duration time.Duration) string { - source := sourceProfile(profile, s.Profiles) + source := s.Profiles.SourceProfile(profile) hasher := md5.New() hasher.Write([]byte(duration.String())) @@ -80,7 +80,7 @@ func (s *KeyringSessions) Delete(profile string) (n int, err error) { } for _, k := range keys { - if strings.HasPrefix(k, fmt.Sprintf("%s session", sourceProfile(profile, s.Profiles))) { + if strings.HasPrefix(k, fmt.Sprintf("%s session", s.Profiles.SourceProfile(profile))) { if err = s.Keyring.Remove(k); err != nil { return n, err }