From f661107a8034ae9325a943b546d5c7750d0c43e3 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Mar 2023 18:00:37 +0200 Subject: [PATCH] feat: improvements (#4) Allow credentials to be passed, general improvements --- .gitignore | 1 + .vscode/launch.json | 27 +++++++++++++++++++++++++ cmd/list.go | 2 +- cmd/login.go | 48 +++++++++++++++++++++++++++++++++------------ cmd/root.go | 17 +++++++++++++--- config.yaml.example | 15 +++++++++++++- utils/prompt.go | 17 ++++++++++------ 7 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 19b8023..ac3343b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config.yaml /onelogin-auth-* .DS_Store +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b5e1113 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "login", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "console": "integratedTerminal", + "args": ["login"], + "envFile": "${workspaceFolder}/.env" + }, + { + "name": "list", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "console": "integratedTerminal", + "args": ["list"] + } + ] +} diff --git a/cmd/list.go b/cmd/list.go index 1e9fd76..2d3aca2 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -18,7 +18,7 @@ var listCmd = &cobra.Command{ fmt.Println("Accounts:") for k, v := range config.Accounts { - fmt.Printf("[%d] %s\n", k, v) + fmt.Printf("[%d] %s\n", k, v.Name) } }, } diff --git a/cmd/login.go b/cmd/login.go index 0f4006b..ff53091 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -24,7 +24,13 @@ var loginCmd = &cobra.Command{ //Get Role and Accounts from parameters or from keyboard input if len(args) != 2 { role, err = getRole(config.Roles) + if err != nil { + log.Fatalln(err) + } account, err = getAccount(config.Accounts) + if err != nil { + log.Fatalln(err) + } } else { roleNum, err := strconv.Atoi(args[0]) if err != nil { @@ -58,13 +64,24 @@ var loginCmd = &cobra.Command{ } //Get email and password from keyboard input - email, err := utils.PromptForString("Email") - if err != nil { - log.Fatalln(err) + var email string + if config.Credentials.Email == "" { + email, err = utils.PromptForString("Email") + if err != nil { + log.Fatalln(err) + } + } else { + email = config.Credentials.Email } - password, err := utils.PromptForSecretString("Password") - if err != nil { - log.Fatalln(err) + + var password string + if config.Credentials.Password == "" { + password, err = utils.PromptForSecretString("Password") + if err != nil { + log.Fatalln(err) + } + } else { + password = config.Credentials.Password } //SAML Assertion and MFA Devices retrieval @@ -82,10 +99,17 @@ var loginCmd = &cobra.Command{ if err != nil { log.Fatalln(err) } - mfaCode, err := utils.PromptForSecretString("MFA Code") - if err != nil { - log.Fatalln(err) + + var mfaCode string + if config.Credentials.OTP == "" { + mfaCode, err = utils.PromptForSecretString("MFA Code") + if err != nil { + log.Fatalln(err) + } + } else { + mfaCode = config.Credentials.OTP } + verificationResponse, err := onelogin.VerifyFactor(token, *deviceID, appID, assertionResponse.StateToken, mfaCode) if err != nil { fmt.Println(err) @@ -126,7 +150,7 @@ var loginCmd = &cobra.Command{ func getRole(roles []string) (*int, error) { - roleName, err := utils.PromptSelect("Role", config.Roles) + roleName, err := utils.PromptSelect("Role", config.Roles, false) if err != nil { return nil, err } @@ -143,7 +167,7 @@ func getAccount(accounts []Account) (*int, error) { for _, v := range accounts { accountsName = append(accountsName, v.Name) } - accountName, err := utils.PromptSelect("Account", accountsName) + accountName, err := utils.PromptSelect("Account", accountsName, false) if err != nil { return nil, err } @@ -160,7 +184,7 @@ func getDeviceID(devices []onelogin.Device) (*int, error) { for _, v := range devices { deviceTypes = append(deviceTypes, v.DeviceType) } - selectedDeviceType, err := utils.PromptSelect("MFA Device", deviceTypes) + selectedDeviceType, err := utils.PromptSelect("MFA Device", deviceTypes, true) if err != nil { log.Fatalln(err) } diff --git a/cmd/root.go b/cmd/root.go index bf610ae..69684ca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,9 +9,16 @@ import ( type Config struct { Onelogin OneLoginConf - Accounts []Account `yaml:"accounts"` - Roles []string `yaml:"roles"` - DefaultRegion string `yaml:"defaultRegion"` + Credentials Credentials `yaml:"credentials"` + Accounts []Account `yaml:"accounts"` + Roles []string `yaml:"roles"` + DefaultRegion string `yaml:"defaultRegion"` +} + +type Credentials struct { + Email string `yaml:"email"` + Password string `yaml:"password"` + OTP string `yaml:"otp"` } type OneLoginConf struct { @@ -59,6 +66,10 @@ func LoadConfig(path string) (config Config, err error) { return } + viper.BindEnv("credentials.email", "EMAIL") + viper.BindEnv("credentials.password", "PASSWORD") + viper.BindEnv("credentials.otp", "OTP") + err = viper.Unmarshal(&config) return } diff --git a/config.yaml.example b/config.yaml.example index ca64146..cee139e 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -10,4 +10,17 @@ accounts: roles: - iam-role-1 # role that is configured in onelogin and IAM to use with the onelogin identity provider - iam-role-2 -defaultRegion: us-east-1 \ No newline at end of file +defaultRegion: us-east-1 + +# Credentials can be specified in the YAML config file, but it is not recommended +# because it will store the credentials in plain text on your disk. +# It is better to use the environment variables EMAIL, PASSWORD and OTP. +credentials: + # it can be overridden by the EMAIL environment variable + email: email of user to use for authentication + # it makes no sense to use this option in the YAML config file, + # but it can be overridden by the PASSWORD environment variable + password: password of user to use for authentication + # it makes no sense to use this option in the YAML config file, + # but it can be overridden by the OTP environment variable + otp: otpToken of user to use for authentication diff --git a/utils/prompt.go b/utils/prompt.go index f2beec1..cbe1136 100644 --- a/utils/prompt.go +++ b/utils/prompt.go @@ -2,9 +2,10 @@ package utils import ( "fmt" - "github.com/manifoldco/promptui" "log" "strconv" + + "github.com/manifoldco/promptui" ) func PromptForInt(label string) (*int, error) { @@ -15,7 +16,7 @@ func PromptForInt(label string) (*int, error) { } prompt := promptui.Prompt{ - Label: label + ":", + Label: label, Validate: validate, } @@ -43,7 +44,7 @@ func PromptForString(label string) (string, error) { } prompt := promptui.Prompt{ - Label: label + ":", + Label: label, Validate: validate, } @@ -67,7 +68,7 @@ func PromptForSecretString(label string) (string, error) { } prompt := promptui.Prompt{ - Label: label + ":", + Label: label, Validate: validate, Mask: rune('*'), } @@ -82,10 +83,14 @@ func PromptForSecretString(label string) (string, error) { return result, nil } -func PromptSelect(label string, options []string) (string, error) { +func PromptSelect(label string, options []string, skipSingleChoice bool) (string, error) { + + if skipSingleChoice && len(options) == 1 { + return options[0], nil + } prompt := promptui.Select{ - Label: label + ":", + Label: label, Items: options, }