From d1b9cdc054c459dd0d46d067adb6cbe7e3f352b9 Mon Sep 17 00:00:00 2001 From: Pavel Vorobyov Date: Tue, 24 Sep 2019 13:42:09 +0300 Subject: [PATCH] Passmgr (#3) * password manager plugin --- cli/cli.go | 35 +++++++++++++++++++++++- cli/completer.go | 2 +- cli/handlers.go | 68 +++++++++++----------------------------------- cli/help.go | 23 ++++++++++++++-- config/config.go | 52 +++++++++++++++++++---------------- passmgr/passmgr.go | 62 ++++++++++++++++++++++++++++++++++++++++++ remote/remote.go | 24 ++++++++++------ remote/runcmd.go | 24 ++++++++++++---- 8 files changed, 196 insertions(+), 94 deletions(-) create mode 100644 passmgr/passmgr.go diff --git a/cli/cli.go b/cli/cli.go index dd485f9..440a3c2 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -13,6 +13,7 @@ import ( "github.com/chzyer/readline" "github.com/viert/xc/config" "github.com/viert/xc/log" + "github.com/viert/xc/passmgr" "github.com/viert/xc/remote" "github.com/viert/xc/store" "github.com/viert/xc/term" @@ -44,6 +45,7 @@ type Cli struct { prependHostnames bool progressBar bool debug bool + usePasswordMgr bool interpreter string sudoInterpreter string @@ -114,6 +116,16 @@ func New(cfg *config.XCConfig, backend store.Backend) (*Cli, error) { cli.outputFileName = "" cli.outputFile = nil + if cfg.PasswordManagerPath != "" { + term.Warnf("Loading password manager from %s\n", cfg.PasswordManagerPath) + err = passmgr.Load(cfg.PasswordManagerPath) + if err != nil { + term.Errorf("Error initializing password manager: %s\n", err) + } else { + cli.usePasswordMgr = true + } + } + remote.Initialize(cli.sshThreads, cli.user) remote.SetPrependHostnames(cli.prependHostnames) remote.SetRemoteTmpdir(cfg.RemoteTmpdir) @@ -276,9 +288,10 @@ func (c *Cli) confirm(msg string) bool { } func (c *Cli) acquirePasswd() { - if c.raiseType == remote.RTNone { + if c.raiseType == remote.RTNone || c.usePasswordMgr { return } + if c.raisePasswd == "" { c.doPasswd("passwd", "") } @@ -407,3 +420,23 @@ func (c *Cli) dorunscript(mode execMode, argsLine string) { r.ErrorHosts = append(r.ErrorHosts, copyError...) r.Print() } + +func doOnOff(propName string, propRef *bool, args []string) { + if len(args) < 1 { + value := "off" + if *propRef { + value = "on" + } + term.Warnf("%s is %s\n", propName, value) + return + } + switch args[0] { + case "on": + *propRef = true + case "off": + *propRef = false + default: + term.Errorf("Invalid %s vaue. Please use either \"on\" or \"off\"\n", propName) + return + } +} diff --git a/cli/completer.go b/cli/completer.go index 3e894d9..f907189 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -40,7 +40,7 @@ func newCompleter(store *store.Store, commands []string) *completer { x.handlers["c_runscript"] = x.completeDistribute x.handlers["p_runscript"] = x.completeDistribute - helpTopics := append(commands, "expressions", "config", "rcfiles") + helpTopics := append(commands, "expressions", "config", "rcfiles", "passmgr") x.handlers["help"] = staticCompleter(helpTopics) return x } diff --git a/cli/handlers.go b/cli/handlers.go index 06dbf89..55d90d3 100644 --- a/cli/handlers.go +++ b/cli/handlers.go @@ -8,6 +8,8 @@ import ( "strconv" "syscall" + "github.com/viert/xc/passmgr" + "github.com/viert/xc/remote" "github.com/viert/xc/term" ) @@ -46,6 +48,7 @@ func (c *Cli) setupCmdHandlers() { c.handlers["s_runscript"] = c.doSRunScript c.handlers["c_runscript"] = c.doCRunScript c.handlers["p_runscript"] = c.doPRunScript + c.handlers["use_password_manager"] = c.doUsePasswordManager commands := make([]string, len(c.handlers)) i := 0 @@ -277,68 +280,29 @@ func (c *Cli) doDelay(name string, argsLine string, args ...string) { } func (c *Cli) doDebug(name string, argsLine string, args ...string) { - if len(args) < 1 { - value := "off" - if c.debug { - value = "on" - } - term.Warnf("Debug is %s\n", value) - return - } - switch args[0] { - case "on": - c.debug = true - case "off": - c.debug = false - default: - term.Errorf("Invalid debug value. Please use either \"on\" or \"off\"\n") - return - } + doOnOff("debug", &c.debug, args) remote.SetDebug(c.debug) } func (c *Cli) doProgressBar(name string, argsLine string, args ...string) { - if len(args) < 1 { - value := "off" - if c.progressBar { - value = "on" - } - term.Warnf("Progressbar is %s\n", value) - return - } - switch args[0] { - case "on": - c.progressBar = true - case "off": - c.progressBar = false - default: - term.Errorf("Invalid progressbar value. Please use either \"on\" or \"off\"\n") - return - } + doOnOff("progressbar", &c.progressBar, args) remote.SetProgressBar(c.progressBar) } func (c *Cli) doPrependHostnames(name string, argsLine string, args ...string) { - if len(args) < 1 { - value := "off" - if c.prependHostnames { - value = "on" - } - term.Warnf("prepend_hostnames is %s\n", value) - return - } - switch args[0] { - case "on": - c.prependHostnames = true - case "off": - c.prependHostnames = false - default: - term.Errorf("Invalid prepend_hostnames value. Please use either \"on\" or \"off\"\n") - return - } + doOnOff("prepend_hostnames", &c.prependHostnames, args) remote.SetPrependHostnames(c.prependHostnames) } +func (c *Cli) doUsePasswordManager(name string, argsLine string, args ...string) { + doOnOff("use_password_manager", &c.usePasswordMgr, args) + if c.usePasswordMgr && !passmgr.Ready() { + term.Errorf("Password manager is not ready\n") + c.usePasswordMgr = false + } + remote.SetUsePasswordManager(c.usePasswordMgr) +} + func (c *Cli) doReload(name string, argsLine string, args ...string) { err := c.store.BackendReload() if err != nil { @@ -359,7 +323,7 @@ func (c *Cli) doInterpreter(name string, argsLine string, args ...string) { func (c *Cli) doConnectTimeout(name string, argsLine string, args ...string) { if len(args) < 1 { - term.Warnf("connect_timeout = %s\n", c.connectTimeout) + term.Warnf("connect_timeout = %d\n", c.connectTimeout) return } ct, err := strconv.ParseInt(args[0], 10, 64) diff --git a/cli/help.go b/cli/help.go index e9e87a9..4340e77 100644 --- a/cli/help.go +++ b/cli/help.go @@ -161,6 +161,16 @@ It may be useful for configuring aliases (as they are dropped when xc exits) and Rcfile is just a number of xc commands in a text file.`, }, + "passmgr": &helpItem{ + isTopic: true, + help: `Password manager is a golang plugin which must have two exported functions: +func Init() error, which is called on xc start +func GetPass(host string) (password string), which is kinda self-explanatory + +For more info on how to write golang plugins, please refer to golang documentation or this article: +https://medium.com/learning-the-go-programming-language/writing-modular-go-programs-with-plugins-ec46381ee1a9`, + }, + "debug": &helpItem{ usage: "", help: `An internal debug. May cause unexpected output. One shouldn't use it unless she knows what she's doing.`, @@ -318,6 +328,12 @@ xc moves on to the next server.`, without arguments, prints the current value.`, }, + "use_password_manager": &helpItem{ + usage: "[]", + help: `Sets the password manager on/off. If no value is given, prints the current value. +If password manager is not ready, setting this value to "on" will print an error.`, + }, + "user": &helpItem{ usage: "", help: `Sets the username for all the execution commands. This is used to get access to hosts via ssh/scp.`, @@ -370,7 +386,8 @@ List of commands: reload reloads hosts and groups data from inventoree runscript runs a local script on a number of remote hosts serial shortcut for "mode serial" - ssh starts ssh session to a number of hosts sequentally - user sets current user -`) + ssh starts ssh session to a number of hosts sequentally + use_password_manager turns password manager on/off + user sets current user`) + fmt.Println() } diff --git a/config/config.go b/config/config.go index 72970d1..14dd3e8 100644 --- a/config/config.go +++ b/config/config.go @@ -39,6 +39,9 @@ interpreter_su = su - type = conductor url = http://c.inventoree.ru work_groups = + +[passmgr] +path = ` // BackendType is a backend type enum @@ -61,28 +64,29 @@ type BackendConfig struct { // XCConfig represents a configuration struct for XC type XCConfig struct { - Readline *readline.Config - BackendCfg *BackendConfig - User string - SSHThreads int - SSHConnectTimeout int - PingCount int - RemoteTmpdir string - Mode string - RaiseType string - Delay int - RCfile string - CacheDir string - CacheTTL time.Duration - Debug bool - ProgressBar bool - PrependHostnames bool - LogFile string - ExitConfirm bool - ExecConfirm bool - SudoInterpreter string - SuInterpreter string - Interpreter string + Readline *readline.Config + BackendCfg *BackendConfig + User string + SSHThreads int + SSHConnectTimeout int + PingCount int + RemoteTmpdir string + Mode string + RaiseType string + Delay int + RCfile string + CacheDir string + CacheTTL time.Duration + Debug bool + ProgressBar bool + PrependHostnames bool + LogFile string + ExitConfirm bool + ExecConfirm bool + SudoInterpreter string + SuInterpreter string + Interpreter string + PasswordManagerPath string } const ( @@ -310,5 +314,7 @@ func read(filename string, secondPass bool) (*XCConfig, error) { return nil, fmt.Errorf("Error configuring backend: backend type is not defined") } - return cfg, err + cfg.PasswordManagerPath, _ = props.GetString("passmgr.path") + + return cfg, nil } diff --git a/passmgr/passmgr.go b/passmgr/passmgr.go new file mode 100644 index 0000000..6ce9a02 --- /dev/null +++ b/passmgr/passmgr.go @@ -0,0 +1,62 @@ +package passmgr + +import ( + "fmt" + "plugin" +) + +type initFunc func() error +type acquireFunc func(string) string + +const ( + initFuncName = "Init" + acquireFuncName = "GetPass" +) + +var ( + p *plugin.Plugin + initialized bool + pluginInit initFunc + pluginAcquire acquireFunc +) + +// Load loads a password manager library +func Load(filename string) error { + var err error + p, err = plugin.Open(filename) + if err != nil { + return err + } + + init, err := p.Lookup(initFuncName) + if err != nil { + return err + } + pluginInit, initialized = init.(initFunc) + if !initialized { + return fmt.Errorf("invalid plugin `%s() error` function signature", initFuncName) + } + + acq, err := p.Lookup(acquireFuncName) + if err != nil { + return err + } + pluginAcquire, initialized = acq.(acquireFunc) + if !initialized { + return fmt.Errorf("invalid plugin `%s(string) string` function signature", acquireFuncName) + } + return nil +} + +// GetPass returns password for a given host from password manager +func GetPass(hostname string) string { + if !initialized { + return "" + } + return pluginAcquire(hostname) +} + +// Ready returns a bool value indicating if the passmgr is initialized and ready to use +func Ready() bool { + return initialized +} diff --git a/remote/remote.go b/remote/remote.go index 2c00f0e..e588212 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -10,15 +10,16 @@ import ( ) var ( - pool *Pool - currentUser string - currentPassword string - currentRaise RaiseType - currentProgressBar bool - currentPrependHostnames bool - currentRemoteTmpdir string - currentDebug bool - outputFile *os.File + pool *Pool + currentUser string + currentPassword string + currentRaise RaiseType + currentUsePasswordManager bool + currentProgressBar bool + currentPrependHostnames bool + currentRemoteTmpdir string + currentDebug bool + outputFile *os.File noneInterpreter string suInterpreter string @@ -83,6 +84,11 @@ func SetPrependHostnames(prependHostnames bool) { currentPrependHostnames = prependHostnames } +// SetUsePasswordManager sets using passmgr on/off +func SetUsePasswordManager(usePasswordMgr bool) { + currentUsePasswordManager = usePasswordMgr +} + // SetConnectTimeout sets the ssh connect timeout in sshOptions func SetConnectTimeout(timeout int) { sshOptions["ConnectTimeout"] = fmt.Sprintf("%d", timeout) diff --git a/remote/runcmd.go b/remote/runcmd.go index 903a110..e802ef2 100644 --- a/remote/runcmd.go +++ b/remote/runcmd.go @@ -10,14 +10,17 @@ import ( "github.com/kr/pty" "github.com/npat-efault/poller" "github.com/viert/xc/log" + "github.com/viert/xc/passmgr" ) func (w *Worker) runcmd(task *Task) int { - var err error - var n int - var passwordSent bool + var ( + err error + n int + password string + passwordSent bool + ) - passwordSent = currentRaise == RTNone cmd := createSSHCmd(task.Hostname, task.Cmd) cmd.Env = append(os.Environ(), environment...) @@ -38,6 +41,17 @@ func (w *Worker) runcmd(task *Task) int { shouldSkipEcho := false msgCount := 0 + if currentRaise != RTNone { + passwordSent = false + if currentUsePasswordManager { + password = passmgr.GetPass(task.Hostname) + } else { + password = currentPassword + } + } else { + passwordSent = true + } + execLoop: for { if w.forceStopped() { @@ -68,7 +82,7 @@ execLoop: // Trying to find Password prompt in first 5 chunks of data from server if msgCount < 5 { if !passwordSent && exPasswdPrompt.Match(chunk) { - ptmx.Write([]byte(currentPassword + "\n")) + ptmx.Write([]byte(password + "\n")) passwordSent = true shouldSkipEcho = true continue