Skip to content

Commit

Permalink
Feature/profiles command (#34)
Browse files Browse the repository at this point in the history
* initial pass

* more porting

* clean up linting

* update deps

* fix confirm-text prompt

* initial rotate support

* fix double-confirm

* set rotator store

* fix readme
  • Loading branch information
akerl committed Mar 3, 2020
1 parent 9a48c87 commit 9dd33d9
Show file tree
Hide file tree
Showing 19 changed files with 1,048 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
@@ -1,7 +1,7 @@
voyager
=========

[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/akerl/voyager/Build)](https://github.com/akerl/voyager/actions))
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/akerl/voyager/Build)](https://github.com/akerl/voyager/actions)
[![GitHub release](https://img.shields.io/github/release/akerl/voyager.svg)](https://github.com/akerl/voyager/releases)
[![License](https://img.shields.io/github/license/akerl/voyager)](https://github.com/akerl/voyager/blob/master/LICENSE)

Expand Down
89 changes: 86 additions & 3 deletions cartogram/account.go
@@ -1,5 +1,18 @@
package cartogram

import (
"regexp"
)

const (
// roleSourceRegexString matches an account number and role name, /-delimited
// Per https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-limits.html .
// role names can contain alphanumeric characters, and these symbols: +=,.@_-
sourceRegexString = `^(\d{12})/([a-zA-Z0-9+=,.@_-]+)$`
)

var sourceRegex = regexp.MustCompile(sourceRegexString)

// AccountSet is a set of accounts
type AccountSet []Account

Expand All @@ -16,11 +29,14 @@ type RoleSet []Role

// Role holds information about authenticating to a role
type Role struct {
Name string `json:"name"`
Mfa bool `json:"mfa"`
Sources []Source `json:"sources"`
Name string `json:"name"`
Mfa bool `json:"mfa"`
Sources SourceSet `json:"sources"`
}

// SourceSet is a list of Sources
type SourceSet []Source

// Source defines the previous hop for accessing a role
type Source struct {
Path string `json:"path"`
Expand Down Expand Up @@ -49,6 +65,24 @@ func (as AccountSet) Search(tfs TagFilterSet) AccountSet {
return results
}

// AllProfiles returns all unique profiles found
func (as AccountSet) AllProfiles() []string {
res := []string{}
for _, x := range as {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// AllProfiles returns all unique profiles found
func (a Account) AllProfiles() []string {
res := []string{}
for _, x := range a.Roles {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// Lookup searches for a role by name
func (rs RoleSet) Lookup(name string) (bool, Role) {
logger.InfoMsgf("looking up role %s in set", name)
Expand All @@ -59,3 +93,52 @@ func (rs RoleSet) Lookup(name string) (bool, Role) {
}
return false, Role{}
}

// AllProfiles returns all unique profiles found
func (rs RoleSet) AllProfiles() []string {
res := []string{}
for _, x := range rs {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// AllProfiles returns all unique profiles found
func (r Role) AllProfiles() []string {
res := []string{}
for _, x := range r.Sources {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// AllProfiles returns all unique profiles found
func (ss SourceSet) AllProfiles() []string {
res := []string{}
for _, x := range ss {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// AllProfiles returns all unique profiles found
func (s Source) AllProfiles() []string {
if s.IsProfile() {
return []string{s.Path}
}
return []string{}
}

// IsProfile returns true if the source hop is
func (s Source) IsProfile() bool {
return !sourceRegex.MatchString(s.Path)
}

// Parse returns the account and role for a non-profile Path, or two empty strings
func (s Source) Parse() (string, string) {
if s.IsProfile() {
return "", ""
}
match := sourceRegex.FindStringSubmatch(s.Path)
return match[1], match[2]
}
9 changes: 9 additions & 0 deletions cartogram/cartogram.go
Expand Up @@ -38,6 +38,15 @@ func (c Cartogram) Search(tfs TagFilterSet) AccountSet {
return c.AccountSet.Search(tfs)
}

// AllProfiles returns all unique profiles found
func (c Cartogram) AllProfiles() []string {
res := []string{}
for _, x := range c.AccountSet {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

func (c *Cartogram) loadFromFile(filePath string) error {
logger.InfoMsgf("loading cartogram from %s", filePath)
data, err := ioutil.ReadFile(filePath)
Expand Down
12 changes: 12 additions & 0 deletions cartogram/main.go
Expand Up @@ -37,3 +37,15 @@ func homeDir() (string, error) {
}
return usr.HomeDir, nil
}

func uniqCollect(input []string) []string {
resultsMap := map[string]bool{}
for _, item := range input {
resultsMap[item] = true
}
results := []string{}
for result := range resultsMap {
results = append(results, result)
}
return results
}
17 changes: 17 additions & 0 deletions cartogram/pack.go
Expand Up @@ -113,6 +113,23 @@ func (cp Pack) Search(tfs TagFilterSet) AccountSet {
return results
}

func (cp Pack) toSlice() []Cartogram {
result := []Cartogram{}
for _, v := range cp {
result = append(result, v)
}
return result
}

// AllProfiles returns all unique profiles found
func (cp Pack) AllProfiles() []string {
res := []string{}
for _, x := range cp {
res = append(res, x.AllProfiles()...)
}
return uniqCollect(res)
}

// Load populates the Cartograms from disk
func (cp Pack) Load() error {
logger.InfoMsg("loading pack from disk")
Expand Down
25 changes: 25 additions & 0 deletions cmd/profiles.go
@@ -0,0 +1,25 @@
package cmd

import (
"github.com/akerl/voyager/v2/cartogram"

"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(profilesCmd)
}

var profilesCmd = &cobra.Command{
Use: "profiles",
Short: "manage stored AWS credentials",
}

func getAllProfiles() ([]string, error) {
pack := cartogram.Pack{}
if err := pack.Load(); err != nil {
return []string{}, err
}

return pack.AllProfiles(), nil
}
58 changes: 58 additions & 0 deletions cmd/profiles_add.go
@@ -0,0 +1,58 @@
package cmd

import (
"fmt"

"github.com/akerl/voyager/v2/profiles"

"github.com/akerl/input/list"
"github.com/spf13/cobra"
)

func init() {
profilesCmd.AddCommand(profilesAddCmd)
}

var profilesAddCmd = &cobra.Command{
Use: "add",
Short: "add new AWS credentials",
RunE: profilesAddRunner,
}

func profilesAddRunner(_ *cobra.Command, args []string) error {
var inputProfile string
if len(args) != 0 {
inputProfile = args[0]
}

store := profiles.NewDefaultStore()

allProfiles, err := getAllProfiles()
if err != nil {
return err
}

profile, err := list.WithInputString(
list.Default(),
allProfiles,
inputProfile,
"Profile to add",
)
if err != nil {
return err
}

check := store.Check(profile)
if check {
fmt.Println(
"Profile is already stored; if you wish to update it, use the rotate command. " +
"If you want to remove it, use the remove command",
)
return nil
}
_, err = store.Lookup(profile)
if err == nil {
fmt.Println("Successfully added profile")
}
return err
}
49 changes: 49 additions & 0 deletions cmd/profiles_delete.go
@@ -0,0 +1,49 @@
package cmd

import (
"fmt"

"github.com/akerl/voyager/v2/profiles"
"github.com/akerl/voyager/v2/utils"

"github.com/spf13/cobra"
)

func init() {
profilesCmd.AddCommand(profilesDeleteCmd)
}

var profilesDeleteCmd = &cobra.Command{
Use: "delete PROFILE",
Short: "delete a stored AWS credential",
RunE: profilesDeleteRunner,
}

func profilesDeleteRunner(_ *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("no profile name provided")
}
profile := args[0]

store := profiles.NewDefaultStore()

check := store.Check(profile)
if !check {
fmt.Printf("No credentials stored for profile: %s\n", profile)
return nil
}

err := utils.ConfirmText(
"this is a destructive operation",
fmt.Sprintf("This will delete the following profile: %s", profile),
)
if err != nil {
return err
}

err = store.Delete(profile)
if err == nil {
fmt.Println("Deleted stored profile")
}
return err
}
45 changes: 45 additions & 0 deletions cmd/profiles_list.go
@@ -0,0 +1,45 @@
package cmd

import (
"fmt"
"sort"

"github.com/akerl/voyager/v2/profiles"

"github.com/spf13/cobra"
)

func init() {
profilesCmd.AddCommand(profilesListCmd)
}

var profilesListCmd = &cobra.Command{
Use: "list",
Short: "list stored AWS credentials",
RunE: profilesListRunner,
}

func profilesListRunner(_ *cobra.Command, _ []string) error {
allProfiles, err := getAllProfiles()
if err != nil {
return err
}

store := profiles.NewDefaultStore()
existing := profiles.BulkCheck(store, allProfiles)

if len(existing) == 0 {
fmt.Println("No credentials found")
return nil
}

sort.Strings(existing)
for _, item := range existing {
creds, err := store.Lookup(item)
if err != nil {
return err
}
fmt.Printf("%s (%s)\n", item, creds.AccessKeyID)
}
return nil
}
38 changes: 38 additions & 0 deletions cmd/profiles_rotate.go
@@ -0,0 +1,38 @@
package cmd

import (
"github.com/akerl/voyager/v2/profiles"
"github.com/akerl/voyager/v2/rotate"

"github.com/spf13/cobra"
)

func init() {
profilesCmd.AddCommand(profilesRotateCmd)
profilesRotateCmd.Flags().BoolP("yubikey", "y", false, "Store MFA on yubikey")
}

var profilesRotateCmd = &cobra.Command{
Use: "rotate",
Short: "saves a new AWS keypair and MFA device from existing creds",
RunE: profilesRotateRunner,
}

func profilesRotateRunner(cmd *cobra.Command, args []string) error {
var inputProfile string
if len(args) != 0 {
inputProfile = args[0]
}

useYubikey, err := cmd.Flags().GetBool("yubikey")
if err != nil {
return err
}

r := rotate.Rotator{
UseYubikey: useYubikey,
InputProfile: inputProfile,
Store: profiles.NewDefaultStore(),
}
return r.Execute()
}

0 comments on commit 9dd33d9

Please sign in to comment.