diff --git a/.md/previewer.gif b/.md/previewer.gif new file mode 100644 index 0000000..a11f5ae Binary files /dev/null and b/.md/previewer.gif differ diff --git a/README.md b/README.md index 442a7e5..eba4f2a 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@
+ +
Logo

AWS SSO Creds

diff --git a/go.mod b/go.mod index 8673273..9fbd6a5 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,8 @@ require ( gopkg.in/ini.v1 v1.62.0 ) +require github.com/deckarep/golang-set v1.8.0 + require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 8e2f4f8..07a69a3 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= diff --git a/internal/app/sso.go b/internal/app/sso.go index 5d22118..272d3a7 100644 --- a/internal/app/sso.go +++ b/internal/app/sso.go @@ -273,7 +273,7 @@ func (s *SSOFlow) GetCredentials() ([]CredentialsResult, error) { }) continue } - profName := tempCredsPrefix + strings.Split(item.roleName, " ")[1] + profName := tempCredsPrefix + strings.TrimPrefix(item.roleName, "profile ") credsSection, err := creds.File.NewSection(profName) if err != nil { return nil, item.err diff --git a/internal/pkg/ui/fuzzyfinder.go b/internal/pkg/ui/fuzzyfinder.go index fd990d8..182aed6 100644 --- a/internal/pkg/ui/fuzzyfinder.go +++ b/internal/pkg/ui/fuzzyfinder.go @@ -1,6 +1,7 @@ package ui import ( + "errors" "fmt" "sort" "strconv" @@ -8,78 +9,165 @@ import ( "time" "github.com/bigkevmcd/go-configparser" + mapset "github.com/deckarep/golang-set" "github.com/ktr0731/go-fuzzyfinder" ) -func fuzzyPreviewer(credentialsPath string, rolesPath string) string { - var selected string - creds, err := configparser.NewConfigParserFromFile(credentialsPath) +const ( + VALID_TEXT = "Valid" + EXPIRED_TEXT = "Expired" +) +type FuzzyPreviewer struct { + entries *configparser.ConfigParser + outputSections []string + rolesMapping *map[string]string +} + +func NewFuzzyPreviewer(credentialsPath string, rolesPath string) (*FuzzyPreviewer, error) { + creds, err := configparser.NewConfigParserFromFile(credentialsPath) + if err != nil { + return nil, errors.New(fmt.Sprintf("Cannot parse file %s: %v", credentialsPath, err)) + } roles, err := configparser.NewConfigParserFromFile(rolesPath) + if err != nil { + return nil, errors.New(fmt.Sprintf("Cannot parse file %s: %v", rolesPath, err)) + } - sections := creds.Sections() - sections = append(sections, roles.Sections()...) - sort.Strings(sections) - _, err = fuzzyfinder.FindMulti( - sections, - func(i int) string { - return sections[i] - }, - fuzzyfinder.WithPreviewWindow(func(i, w, h int) string { - if i == -1 { - return "" - } - selected = sections[i] - s := fmt.Sprintf("[%s]\n", selected) - - // if is a profile (~/.aws/confg) - var aux configparser.Dict - var showedKeys configparser.Dict = make(configparser.Dict) - if strings.HasPrefix(selected, "profile") { - aux, err = roles.Items(selected) + rolesMapping := map[string]string{} + + outputSections := []string{} + extraSections := mapset.NewSet() + entries := configparser.New() + + // Go though the sections of the credentials file (~/.aws/credentials) + for _, sec := range creds.Sections() { + err := entries.AddSection(sec) + if err != nil { + return nil, errors.New(fmt.Sprintf("Cannot add section %s: %v", sec, err)) + } + extraSections.Add(sec) + items, err := creds.Items(sec) + if err != nil { + return nil, errors.New(fmt.Sprintf("Cannot get the %s entries from the credentials file (~/.aws/credentials): %v", sec, err)) + } + + rolesMapping[sec] = sec + for k, v := range items { + entries.Set(sec, k, v) + } + } + + var outputName string + var extraItems configparser.Dict + + // Go though the sections of the config file (~/.aws/config) + for _, sec := range roles.Sections() { + profileName := strings.TrimPrefix(sec, "profile ") + _, err := roles.GetBool(sec, "sso_auto_populated") + // If is not autopopulated + if err != nil { + if entries.HasSection(profileName) { + extraItems, _ = entries.Items(profileName) + err = entries.RemoveSection(profileName) if err != nil { - return "" + return nil, errors.New(fmt.Sprintf("Cannot erase section %s from configFile: %v", profileName, err)) } - showedKeys["Account name"] = aux["sso_account_name"] - showedKeys["Account ID"] = aux["sso_account_id"] - showedKeys["Region"] = aux["region"] + extraSections.Remove(profileName) + } + outputName = fmt.Sprintf("(profile) %s", profileName) + } else { + outputName = fmt.Sprintf("(SSO profile) %s", profileName) + } + + rolesMapping[outputName] = sec + + outputSections = append(outputSections, outputName) + entries.AddSection(sec) + + items, _ := roles.Items(sec) + for k, v := range items { + entries.Set(sec, k, v) + } + for k, v := range extraItems { + entries.Set(sec, k, v) + } + } + + for sec := range extraSections.Iter() { + outputSections = append(outputSections, sec.(string)) + } + return &FuzzyPreviewer{ + rolesMapping: &rolesMapping, + entries: entries, + outputSections: outputSections, + }, nil +} + +func (fp *FuzzyPreviewer) generatePreviewAttrs(selected string) (*string, error) { + s := fmt.Sprintf("[%s]\n", (*fp.rolesMapping)[selected]) + items, _ := fp.entries.Items((*fp.rolesMapping)[selected]) + keys := sort.StringSlice(items.Keys()) + for _, k := range keys { + v := items[k] + switch k { + case "aws_secret_access_key", "aws_session_token": + continue + case "expires_time": + exp, err := strconv.Atoi(v) + if err != nil { + s += fmt.Sprintln(fmt.Sprintf("Cannot parse: %s", k)) + } + expiresAt := time.Unix(int64(exp), 0) + s += fmt.Sprintln(fmt.Sprintf("Expires Time: %s", expiresAt.String())) + var expiredTxt string + if expiresAt.Before(time.Now()) { + expiredTxt = EXPIRED_TEXT } else { - aux, err = creds.Items(selected) - if err != nil { - return "" - } - showedKeys["AWS access key id"] = aux["aws_access_key_id"] - iss, err := strconv.Atoi(aux["issued_time"]) - if err != nil { - return "" - } - exp, err := strconv.Atoi(aux["expires_time"]) - if err != nil { - return "" - } - expiredAt := time.Unix(int64(exp), 0) - showedKeys["Issued at"] = time.Unix(int64(iss), 0).String() - showedKeys["Expires at"] = expiredAt.String() - if expiredAt.Before(time.Now()) { - showedKeys["Status"] = "Expiradas" - } else { - showedKeys["Status"] = "VĂ¡lidas" - } + expiredTxt = VALID_TEXT + } + s += fmt.Sprintln(fmt.Sprintf("Status: %s", expiredTxt)) + break + case "issued_time": + iss, err := strconv.Atoi(v) + if err != nil { + s += fmt.Sprintln(fmt.Sprintf("Cannot parse %s", k)) } + issuedAt := time.Unix(int64(iss), 0) + s += fmt.Sprintln(fmt.Sprintf("Issued Time: %s", issuedAt.String())) + break + default: + k = strings.Title(strings.ReplaceAll(k, "_", " ")) + s += fmt.Sprintln(fmt.Sprintf("%s: %s", k, v)) + } + } + return &s, nil +} - for _, key := range showedKeys.Keys() { - s += fmt.Sprintf("%s: %s\n", key, showedKeys[key]) +func (fp *FuzzyPreviewer) Preview() (*string, error) { + var selected string + _, err := fuzzyfinder.FindMulti( + fp.outputSections, + func(i int) string { + return fp.outputSections[i] + }, + fuzzyfinder.WithPreviewWindow(func(i, w, h int) string { + if i == -1 { + return "" + } + selected = fp.outputSections[i] + s, err := fp.generatePreviewAttrs(selected) + if err != nil { + return fmt.Sprintf("Cannot parse attributes from %s", selected) } - return s + return *s })) - parts := strings.Split(selected, " ") - var role string - if len(parts) == 1 { - role = parts[0] - } else { - role = parts[1] + if err != nil { + return nil, err } - return role + + result := strings.TrimPrefix((*fp.rolesMapping)[selected], "profile ") + return &result, nil } diff --git a/internal/pkg/ui/ui.go b/internal/pkg/ui/ui.go index 5f9aef3..c04abdf 100644 --- a/internal/pkg/ui/ui.go +++ b/internal/pkg/ui/ui.go @@ -79,14 +79,23 @@ func (u *UI) Start() error { } if u.UsePreviewer { - selectedEntry := fuzzyPreviewer(credentialsPath, configFilePath) - fmt.Println(selectedEntry) + fp, err := NewFuzzyPreviewer(credentialsPath, configFilePath) + if err != nil { + fmt.Println(fmt.Sprintf("Error starting program: %s", err)) + os.Exit(1) + } + selectedEntry, err := fp.Preview() + if err != nil { + fmt.Println(fmt.Sprintf("Error Selecting entry: %s", err)) + os.Exit(1) + } + fmt.Println(*selectedEntry) } else { m := initialModel() p := tea.NewProgram(m) go u.handleFlow() if err := p.Start(); err != nil { - fmt.Printf("Error starting program: %s\n", err) + fmt.Println(fmt.Sprintf("Error starting program: %s", err)) os.Exit(1) } } diff --git a/testdata/config.ini b/testdata/config.ini new file mode 100644 index 0000000..542c996 --- /dev/null +++ b/testdata/config.ini @@ -0,0 +1,74 @@ +[profile tmp:Account 5:Role 2] +region = us-east-1 + +[profile Account 1:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 5 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true + +[profile Account 2:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 2 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true + +[profile Account 3:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 3 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true + +[profile Account 4:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 4 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true + +[profile Account 5:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 5 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true + +[profile Account 5:Role 2] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 5 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 2 +region = us-east-1 +sso_auto_populated = true + +[profile Account 5:Role 3] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 5 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 3 +region = us-east-1 +sso_auto_populated = true + +[profile Account 6:Role 1] +sso_start_url = https://myApp.awsapps.com/start +sso_region = us-east-1 +sso_account_name = Account 6 +sso_account_id = XXXXXXXXXXXX +sso_role_name = Role 1 +region = us-east-1 +sso_auto_populated = true