Mercurial > gemma
view controllers/pwreset.go @ 345:b97b3172c61a
Add staging feature to more tables
Added tables currently only have limited visibility for waterway_user
but not yet policies for write access.
author | Tom Gottfried <tom@intevation.de> |
---|---|
date | Mon, 06 Aug 2018 15:19:05 +0200 |
parents | 33b59c848771 |
children | ac23905e64b1 |
line wrap: on
line source
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/auth" "gemma.intevation.de/gemma/config" "gemma.intevation.de/gemma/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(misc.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 misc.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 }