photoprism/internal/commands/passwd.go
Michael Mayer 163398b76c Passwords: Enforce 72-character limit and improve bcrypt support #1987
Signed-off-by: Michael Mayer <michael@photoprism.app>
2023-04-13 17:21:18 +02:00

162 lines
3.7 KiB
Go

package commands
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
)
// PasswdCommand configures the command name, flags, and action.
var PasswdCommand = cli.Command{
Name: "passwd",
Usage: "Changes the password of the user specified as argument",
ArgsUsage: "[username]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "show, s",
Usage: "show bcrypt password hash",
},
},
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.Deleted() {
return fmt.Errorf("user %s has been deleted", clean.LogQuote(id))
}
log.Infof("please enter a new password for %s (minimum %d characters)\n", clean.Log(m.Username()), entity.PasswordLength)
newPassword := getPassword("New Password: ")
if len(newPassword) < 6 {
return errors.New("new password is too short, please try again")
}
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 successfully changed to %s\n", clean.Log(m.Username()), pw.Hash)
} else {
log.Infof("password for %s successfully 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)
}