package commands import ( "bufio" "context" "errors" "fmt" "os" "os/signal" "strings" "syscall" "github.com/urfave/cli/v2" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/txt" ) // PasswdCommand configures the command name, flags, and action. var PasswdCommand = &cli.Command{ Name: "passwd", Usage: "Changes the local account password of a registered user", ArgsUsage: "[username]", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "show", Aliases: []string{"s"}, Usage: "shows bcrypt hash of new password", }, &cli.BoolFlag{ Name: "remove", Aliases: []string{"rm"}, Usage: "removes password to disable local authentication", }, }, Action: passwdAction, } // passwdAction changes the password of the user specified as command argument. func passwdAction(ctx *cli.Context) error { id := clean.Username(ctx.Args().First()) // Name or UID provided? if id == "" { return cli.ShowSubcommandHelp(ctx) } conf, err := InitConfig(ctx) _, cancel := context.WithCancel(context.Background()) defer cancel() if err != nil { return err } conf.InitDb() defer conf.Shutdown() // Find user record. var m *entity.User if rnd.IsUID(id, entity.UserUID) { m = entity.FindUserByUID(id) } else { m = entity.FindUserByName(id) } if m == nil { return fmt.Errorf("user %s not found", clean.LogQuote(id)) } else if m.IsDeleted() { return fmt.Errorf("user %s has been deleted", clean.LogQuote(id)) } // Remove account password if the --remove or --rm flag is set. if ctx.Bool("remove") { err = m.DeletePassword() if err == nil { log.Infof("password for %s has been removed", clean.Log(m.Username())) } return err } // Otherwise, update the account password. log.Infof("please enter a new password for %s (%d-%d characters)\n", clean.Log(m.Username()), entity.PasswordLength, txt.ClipPassword) newPassword := getPassword("New Password: ") if len([]rune(newPassword)) < entity.PasswordLength { return fmt.Errorf("password must have at least %d characters", entity.PasswordLength) } else if len(newPassword) > txt.ClipPassword { return fmt.Errorf("password must have less than %d characters", txt.ClipPassword) } retypePassword := getPassword("Retype Password: ") if newPassword != retypePassword { return errors.New("passwords did not match, please try again") } if err = m.SetPassword(newPassword); err != nil { return err } // Show bcrypt password hash? if pw := entity.FindPassword(m.UserUID); ctx.Bool("show") && pw != nil { log.Infof("password for %s has been set to %s\n", clean.Log(m.Username()), pw.Hash) } else { log.Infof("password for %s has been changed\n", clean.Log(m.Username())) } return nil } // License: MIT Open Source // Copyright (c) Joe Linoff 2016 // Go code to prompt for password using only standard packages by utilizing syscall.ForkExec() and syscall.Wait4(). // Correctly resets terminal echo after ^C interrupts. // // techEcho() - turns terminal echo on or off. func termEcho(on bool) { // Common settings and variables for both stty calls. attrs := syscall.ProcAttr{ Dir: "", Env: []string{}, Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}, Sys: nil} var ws syscall.WaitStatus cmd := "echo" if on == false { cmd = "-echo" } // Enable/disable echoing. pid, err := syscall.ForkExec( "/bin/stty", []string{"stty", cmd}, &attrs) if err != nil { panic(err) } // Wait for the stty process to complete. _, err = syscall.Wait4(pid, &ws, 0, nil) if err != nil { panic(err) } } // getPassword - Prompt for password. func getPassword(prompt string) string { fmt.Print(prompt) // Catch a ^C interrupt. // Make sure that we reset term echo before exiting. signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, os.Interrupt) go func() { for range signalChannel { fmt.Println("") termEcho(true) os.Exit(1) } }() // Echo is disabled, now grab the data. termEcho(false) // disable terminal echo reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') termEcho(true) // always re-enable terminal echo fmt.Println("") if err != nil { // The terminal has been reset, go ahead and exit. fmt.Println("ERROR:", err.Error()) os.Exit(1) } return strings.TrimSpace(text) }