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 @@
+
+
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