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 console subcommand #93

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
35 changes: 35 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func parseArgs(args []string) (Command, error) {
case "add", "create", "new":
return parseAddArgs(commandArgs[1:])

case "console":
return parseConsoleArgs(commandArgs[1:])

case "cp", "copy":
return parseCopyArgs(commandArgs[1:])

Expand Down Expand Up @@ -185,6 +188,38 @@ func parseAddArgs(args []string) (Command, error) {
return e, nil
}

func parseConsoleArgs(args []string) (Command, error) {
flag := pflag.NewFlagSet("console", pflag.ContinueOnError)
flag.String("assume", "", "Role to assume")
flag.Duration("duration", 0, "Duration (15m-12h)")
flag.Usage = func() {}
err := flag.Parse(args)
if err != nil {
return nil, err
}

vaultName := ""
assume, _ := flag.GetString("assume")
duration, _ := flag.GetDuration("duration")
if duration != 0 && (duration < ConsoleMinDuration || duration > ConsoleMaxDuration) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console*Duration constants aren't specific to the console, but to the underlying Role. These should probably be named/refactored accordingly.

return nil, ErrInvalidDuration
}

if flag.NArg() > 1 {
return nil, ErrTooManyArguments
}

if flag.NArg() == 1 {
vaultName = flag.Arg(0)
}

c := &Console{}
c.VaultName = vaultName
c.Role = assume
c.Duration = duration
return c, nil
}

func parseCopyArgs(args []string) (Command, error) {
flag := pflag.NewFlagSet("copy", pflag.ContinueOnError)
flag.Usage = func() {}
Expand Down
48 changes: 48 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"testing"
"time"
)

type parseCase struct {
Expand Down Expand Up @@ -128,6 +129,38 @@ var (
Command: &Help{Subcommand: "create"},
},

// Console
{
Args: []string{"console", "one"},
Command: &Console{
VaultName: "one",
},
},
{
Args: []string{"console", "--assume", "arn:something:or:other"},
Command: &Console{
Role: "arn:something:or:other",
},
},
{
Args: []string{"console", "--assume", "arn:something:or:other", "one"},
Command: &Console{
VaultName: "one",
Role: "arn:something:or:other",
},
},
{
Args: []string{"console", "--duration", "15m", "one"},
Command: &Console{
VaultName: "one",
Duration: 15 * time.Minute,
},
},
{
Args: []string{"console", "--help"},
Command: &Help{Subcommand: "console"},
},

// Copy
{
Args: []string{"cp", "one", "two"},
Expand Down Expand Up @@ -288,6 +321,10 @@ var (
Args: []string{"help", "upgrade"},
Command: &Help{Subcommand: "upgrade"},
},
{
Args: []string{"help", "console"},
Command: &Help{Subcommand: "console"},
},
{
Args: []string{"-h"},
Command: &Help{},
Expand Down Expand Up @@ -467,6 +504,17 @@ var (
Args: []string{"add", "one", "two"},
},

// Console
{
Args: []string{"console", "one", "two"},
},
{
Args: []string{"console", "--duration", "10m"},
},
{
Args: []string{"console", "--duration", "13h"},
},

// Copy
{
Args: []string{"cp"},
Expand Down
197 changes: 197 additions & 0 deletions console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package main

import (
"errors"
"fmt"
"net/url"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/miquella/vaulted/lib"
"github.com/pkg/browser"
)

var (
ErrInvalidTemporaryCredentials = errors.New("Temporary session credentials found. Console cannot be opened with temporary credentials.\nIf the AWS Key is set in a vault, the permanent credentials can be used by specifying the vault name.")
ErrInvalidDuration = errors.New("Console duration must be between 15m and 12h.")
ErrNoCredentialsFound = errors.New("No credentials found. Console cannot be opened.")
)

const (
ConsoleURL = "https://console.aws.amazon.com/console/home"
ConsoleFederationSigninURL = "https://signin.aws.amazon.com/federation"

ConsoleMinDuration = 15 * time.Minute
ConsoleMaxDuration = 12 * time.Hour
ConsoleDefaultDuration = 1 * time.Hour
)

type Console struct {
VaultName string
Role string
Duration time.Duration
}

type TokenParams struct {
awsKey *vaulted.AWSKey
duration time.Duration
}

func (c *Console) Run(store vaulted.Store) error {
signinToken, err := c.getSigninToken(store)
if err != nil {
return err
}

return openConsole(signinToken)
}

func (c *Console) getSigninToken(store vaulted.Store) (string, error) {
params, err := c.getTokenParams(store)
if err != nil {
return "", err
}

if c.Role != "" {
return c.getAssumeRoleToken(store, params)
} else {
return c.getFederationToken(params)
}
}

func (c *Console) getTokenParams(store vaulted.Store) (TokenParams, error) {
vault, err := c.getVault(store)
if err != nil {
return TokenParams{}, err
}

awsKey := c.validateAWSKey(vault.AWSKey)

duration, err := c.chooseDuration(vault.Duration)
if err != nil {
return TokenParams{}, err
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you new line here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

params := TokenParams{
awsKey: awsKey,
duration: duration,
}
return params, nil
}

func (c *Console) getVault(store vaulted.Store) (*vaulted.Vault, error) {
vault := &vaulted.Vault{}
var err error
if c.VaultName != "" {
vault, _, err = store.OpenVault(c.VaultName)
if err != nil {
return nil, err
}
}
return vault, nil
}

func (c *Console) validateAWSKey(awsKey *vaulted.AWSKey) *vaulted.AWSKey {
key := &vaulted.AWSKey{}

if awsKey != nil && awsKey.Valid() {
key = awsKey
}

if c.Role != "" {
key.Role = c.Role
}
return key
}

func (c *Console) chooseDuration(vaultDuration time.Duration) (time.Duration, error) {
duration := ConsoleDefaultDuration

if vaultDuration != 0 {
duration = vaultDuration
}

if c.Duration != 0 {
duration = c.Duration
}

return capDuration(duration)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we've validated the Duration when we received the command, it still feels like we shouldn't be calling capDuration on the command flag duration. capDuration really only belongs with the Vault duration.

}

func capDuration(duration time.Duration) (time.Duration, error) {
if duration < ConsoleMinDuration {
return time.Duration(0), ErrInvalidDuration
}
if duration > ConsoleMaxDuration {
duration = ConsoleMaxDuration
fmt.Println("Your vault duration is greater than the max console duration.\nCurrent console session duration set to 12 hours.")
}
return duration, nil
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: line gap

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

func (c *Console) getAssumeRoleToken(store vaulted.Store, params TokenParams) (string, error) {
var err error
awsCreds, err := c.getCredentials(params.awsKey)
if err != nil {
return "", err
}

if params.awsKey.MFA != "" {
tokenCode, err := store.Steward().GetMFAToken(c.VaultName)
if err != nil {
return "", err
}
awsCreds, err = awsCreds.AssumeRoleWithMFA(params.awsKey.MFA, tokenCode, params.awsKey.Role, ConsoleMinDuration)
} else {
awsCreds, err = awsCreds.AssumeRole(params.awsKey.Role, ConsoleMinDuration)
}
if err != nil {
return "", err
}
return awsCreds.GetSigninToken(&params.duration)
}

func (c *Console) getFederationToken(params TokenParams) (string, error) {
awsCreds, err := c.getCredentials(params.awsKey)
if err != nil {
return "", err
}

awsCreds, err = awsCreds.GetFederationToken(params.duration)
if err != nil {
return "", err
}
return awsCreds.GetSigninToken(nil)
}

func (c *Console) getCredentials(awsKey *vaulted.AWSKey) (*vaulted.AWSCredentials, error) {
awsCreds, err := awsKey.AWSCredentials.WithLocalDefault()
if err != nil {
if err != credentials.ErrNoValidProvidersFoundInChain {
return nil, err
} else if err == credentials.ErrNoValidProvidersFoundInChain {
return nil, ErrNoCredentialsFound
}
}

if awsCreds.ValidSession() {
return nil, ErrInvalidTemporaryCredentials
}

return awsCreds, nil
}

func openConsole(signinToken string) error {
signinURL, _ := url.Parse(ConsoleFederationSigninURL)
loginQuery := url.Values{
"Action": []string{"login"},
"SigninToken": []string{signinToken},
"Destination": []string{ConsoleURL},
}
signinURL.RawQuery = loginQuery.Encode()
err := browser.OpenURL(signinURL.String())
if err != nil {
return err
}
return nil
}
45 changes: 45 additions & 0 deletions console_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"testing"
"time"

"github.com/miquella/vaulted/lib"
)

func TestConsole(t *testing.T) {
var c Console = Console{
VaultName: "one",
}
var err error
store := NewTestStore()
store.Vaults["one"] = &vaulted.Vault{
AWSKey: &vaulted.AWSKey{},
}
store.Vaults["one"].Duration = ConsoleMinDuration
err = c.Run(store)
if err != ErrNoCredentialsFound {
t.Error("No credentials provided, should have caused an ErrNoCredentialsFound")
}

store.Vaults["one"].AWSKey.AWSCredentials = vaulted.AWSCredentials{
ID: "id",
Secret: "secret",
}
store.Vaults["one"].Duration = 10 * time.Minute
err = c.Run(store)
if err != ErrInvalidDuration {
t.Error("Invalid vault duration, should have caused an ErrInvalidDuration")
}

store.Vaults["one"].AWSKey.AWSCredentials = vaulted.AWSCredentials{
ID: "id",
Secret: "secret",
Token: "token",
}
store.Vaults["one"].Duration = ConsoleMinDuration
err = c.Run(store)
if err != ErrInvalidTemporaryCredentials {
t.Error("Temporary session credentials provided, should have caused an invalid temp credentials error")
}
}
2 changes: 1 addition & 1 deletion doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ gem install md2man
```

```sh
go get -u github.com/jteeuwen/go-bindata
go get -u github.com/jteeuwen/go-bindata/...
```

Generating Man Pages
Expand Down
Loading