Mercurial > gemma
diff pkg/controllers/pwreset.go @ 414:c1047fd04a3a
Moved project specific Go packages to new pkg folder.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Wed, 15 Aug 2018 17:30:50 +0200 |
parents | controllers/pwreset.go@ac23905e64b1 |
children | ffdb507d5b42 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/controllers/pwreset.go Wed Aug 15 17:30:50 2018 +0200 @@ -0,0 +1,279 @@ +package controllers + +import ( + "bytes" + "database/sql" + "encoding/hex" + "log" + "net/http" + "os/exec" + "strings" + "text/template" + "time" + + "github.com/gorilla/mux" + + "gemma.intevation.de/gemma/pkg/auth" + "gemma.intevation.de/gemma/pkg/common" + "gemma.intevation.de/gemma/pkg/config" + "gemma.intevation.de/gemma/pkg/misc" +) + +const ( + insertRequestSQL = `INSERT INTO pw_reset.password_reset_requests + (hash, username) VALUES ($1, $2)` + + countRequestsSQL = `SELECT count(*) FROM pw_reset.password_reset_requests` + + countRequestsUserSQL = `SELECT count(*) FROM pw_reset.password_reset_requests + WHERE username = $1` + + deleteRequestSQL = `DELETE FROM pw_reset.password_reset_requests + WHERE hash = $1` + + findRequestSQL = `SELECT lu.email_address, lu.username + FROM pw_reset.password_reset_requests prr + JOIN pw_reset.list_users lu on prr.username = lu.username + WHERE prr.hash = $1` + + cleanupRequestsSQL = `DELETE FROM pw_reset.password_reset_requests + WHERE issued < $1` + + userExistsSQL = `SELECT email_address + FROM pw_reset.list_users WHERE username = $1` + + updatePasswordSQL = `UPDATE pw_reset.list_users + SET pw = $1 WHERE username = $2` +) + +const ( + hashLength = 16 + passwordLength = 20 + passwordResetValid = 12 * time.Hour + maxPasswordResets = 1000 + maxPasswordRequestsPerUser = 5 + cleanupPause = 15 * time.Minute +) + +var ( + passwordResetRequestMailTmpl = template.Must( + template.New("request").Parse(`You have requested a password change +for your account {{ .User }} on +{{ .HTTPS }}://{{ .Server }} + +Please follow this link to get to the page where you can change your password. + +{{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} + +The link is only valid for 12 hours. + +Best regards + Your service team`)) + + passwordResetMailTmpl = template.Must( + template.New("reset").Parse(`Your password for your account {{ .User }} on +{{ .HTTPS }}://{{ .Server }} + +has been changed to + {{ .Password }} + +Change it as soon as possible. + +Best regards + Your service team`)) +) + +func asServiceUser(fn func(*sql.DB) error) error { + db, err := auth.OpenDB(config.ServiceUser(), config.ServicePassword()) + if err == nil { + defer db.Close() + err = fn(db) + } + return err +} + +func init() { + go removeOutdated() +} + +func removeOutdated() { + for { + time.Sleep(cleanupPause) + err := asServiceUser(func(db *sql.DB) error { + good := time.Now().Add(-passwordResetValid) + _, err := db.Exec(cleanupRequestsSQL, good) + return err + }) + if err != nil { + log.Printf("error: %v\n", err) + } + } +} + +func requestMessageBody(https, user, hash, server string) string { + var content = struct { + User string + HTTPS string + Server string + Hash string + }{ + User: user, + HTTPS: https, + Server: server, + Hash: hash, + } + var buf bytes.Buffer + if err := passwordResetRequestMailTmpl.Execute(&buf, &content); err != nil { + log.Printf("error: %v\n", err) + } + return buf.String() +} + +func changedMessageBody(https, user, password, server string) string { + var content = struct { + User string + HTTPS string + Server string + Password string + }{ + User: user, + HTTPS: https, + Server: server, + Password: password, + } + var buf bytes.Buffer + if err := passwordResetMailTmpl.Execute(&buf, &content); err != nil { + log.Printf("error: %v\n", err) + } + return buf.String() +} + +func useHTTPS(req *http.Request) string { + if strings.ToLower(req.URL.Scheme) == "https" { + return "https" + } + return "http" +} + +func generateHash() string { + return hex.EncodeToString(common.GenerateRandomKey(hashLength)) +} + +func generateNewPassword() string { + // First try pwgen + out, err := exec.Command("pwgen", "-y", "20", "1").Output() + if err == nil { + return strings.TrimSpace(string(out)) + } + // Use internal generator. + return common.RandomString(20) +} + +func passwordResetRequest( + input interface{}, + req *http.Request, + _ *sql.DB, +) (jr JSONResult, err error) { + + user := input.(*PWResetUser) + + if user.User == "" { + err = JSONError{http.StatusBadRequest, "Invalid user name"} + return + } + + var hash, email string + + if err = asServiceUser(func(db *sql.DB) error { + + var count int64 + if err := db.QueryRow(countRequestsSQL).Scan(&count); err != nil { + return err + } + + // Limit total number of password requests. + if count >= maxPasswordResets { + return JSONError{ + Code: http.StatusServiceUnavailable, + Message: "Too much password reset request", + } + } + + err := db.QueryRow(userExistsSQL, user.User).Scan(&email) + + switch { + case err == sql.ErrNoRows: + return JSONError{http.StatusNotFound, "User does not exist."} + case err != nil: + return err + } + + if err := db.QueryRow(countRequestsUserSQL, user.User).Scan(&count); err != nil { + return err + } + + // Limit requests per user + if count >= maxPasswordRequestsPerUser { + return JSONError{ + Code: http.StatusServiceUnavailable, + Message: "Too much password reset requests for user", + } + } + + hash = generateHash() + _, err = db.Exec(insertRequestSQL, hash, user.User) + return err + }); err == nil { + body := requestMessageBody(useHTTPS(req), user.User, hash, req.Host) + + if err = misc.SendMail(email, "Password Reset Link", body); err == nil { + jr.Result = &struct { + SendTo string `json:"send-to"` + }{email} + } + } + return +} + +func passwordReset( + _ interface{}, + req *http.Request, + _ *sql.DB, +) (jr JSONResult, err error) { + + hash := mux.Vars(req)["hash"] + if _, err = hex.DecodeString(hash); err != nil { + err = JSONError{http.StatusBadRequest, "Invalid hash"} + return + } + + var email, user, password string + + if err = asServiceUser(func(db *sql.DB) error { + err := db.QueryRow(findRequestSQL, hash).Scan(&email, &user) + switch { + case err == sql.ErrNoRows: + return JSONError{http.StatusNotFound, "No such hash"} + case err != nil: + return err + } + password = generateNewPassword() + res, err := db.Exec(updatePasswordSQL, password, user) + if err != nil { + return err + } + if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { + return JSONError{http.StatusNotFound, "User not found"} + } + _, err = db.Exec(deleteRequestSQL, hash) + return err + }); err == nil { + body := changedMessageBody(useHTTPS(req), user, password, req.Host) + if err = misc.SendMail(email, "Password Reset Done", body); err == nil { + jr.Result = &struct { + SendTo string `json:"send-to"` + }{email} + } + } + return +}