Mercurial > gemma
view pkg/controllers/pwreset.go @ 1840:e3311c18a8e3
Translated using Weblate (Romanian)
Currently translated at 5.7% (7 of 123 strings)
Translation: Gemma/client
Translate-URL: https://hosted.weblate.org/projects/gemma/client/ro/
author | Fadi Abbud <fadi.abbud@intevation.de> |
---|---|
date | Thu, 17 Jan 2019 11:19:15 +0000 |
parents | cabf4789e02b |
children | ed4820efb7e6 |
line wrap: on
line source
// This is Free Software under GNU Affero General Public License v >= 3.0 // without warranty, see README.md and license for details. // // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // // Copyright (C) 2018 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // // Author(s): // * Sascha L. Teichmann <sascha.teichmann@intevation.de> // * Bernhard E. Reiter <bernhard.reiter@intevation.de> // * Tom Gottfried <tom.gottfried.intevation.de> package controllers import ( "bytes" "context" "database/sql" "encoding/hex" "errors" "log" "net/http" "os/exec" "strconv" "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" "gemma.intevation.de/gemma/pkg/models" ) const ( insertRequestSQL = `INSERT INTO sys_admin.password_reset_requests (hash, username) VALUES ($1, $2)` countRequestsSQL = `SELECT count(*) FROM sys_admin.password_reset_requests` countRequestsUserSQL = `SELECT count(*) FROM sys_admin.password_reset_requests WHERE username = $1` deleteRequestSQL = `DELETE FROM sys_admin.password_reset_requests WHERE hash = $1` findRequestSQL = `SELECT lu.email_address, lu.username FROM sys_admin.password_reset_requests prr JOIN users.list_users lu on prr.username = lu.username WHERE prr.hash = $1` cleanupRequestsSQL = `DELETE FROM sys_admin.password_reset_requests WHERE issued < $1` userExistsSQL = `SELECT email_address FROM users.list_users WHERE username = $1` updatePasswordSQL = `UPDATE users.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 ) const pwResetRole = "sys_admin" var ( errTooMuchPasswordResets = errors.New("Too many password resets") errTooMuchPasswordResetsPerUser = errors.New("Too many password resets per user") errNoSuchUser = errors.New("User does not exist") errInvalidUser = errors.New("Invalid user") ) var ( passwordResetRequestMailTmpl = template.Must( template.New("request").Parse(`You or someone else has requested a password change for your account {{ .User }} on {{ .HTTPS }}://{{ .Server }} Please follow this link to have a new password generated and mailed to you: {{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} The link is only valid for 12 hours. If you did not initiate this password reset or do not want to reset the password, just ignore this email. 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 init() { go removeOutdated() } func removeOutdated() { config.WaitReady() for { time.Sleep(cleanupPause) ctx := context.Background() err := auth.RunAs( ctx, pwResetRole, func(conn *sql.Conn) error { good := time.Now().Add(-passwordResetValid) _, err := conn.ExecContext( ctx, 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 req.Header.Get("X-Use-Protocol") == "https" || 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", strconv.Itoa(passwordLength), "1").Output() if err == nil { return strings.TrimSpace(string(out)) } // Use internal generator. return common.RandomString(passwordLength) } func backgroundRequest(https, host string, user *models.PWResetUser) error { if user.User == "" { return errInvalidUser } var hash, email string ctx := context.Background() if err := auth.RunAs( ctx, pwResetRole, func(conn *sql.Conn) error { var count int64 if err := conn.QueryRowContext( ctx, countRequestsSQL).Scan(&count); err != nil { return err } // Limit total number of password requests. if count >= maxPasswordResets { return errTooMuchPasswordResets } err := conn.QueryRowContext(ctx, userExistsSQL, user.User).Scan(&email) switch { case err == sql.ErrNoRows: return errNoSuchUser case err != nil: return err } if err := conn.QueryRowContext( ctx, countRequestsUserSQL, user.User).Scan(&count); err != nil { return err } // Limit requests per user if count >= maxPasswordRequestsPerUser { return errTooMuchPasswordResetsPerUser } hash = generateHash() _, err = conn.ExecContext(ctx, insertRequestSQL, hash, user.User) return err }, ); err != nil { return err } body := requestMessageBody(https, user.User, hash, host) return misc.SendMail(email, "Password Reset Link", body) } // host checks if we are behind a proxy and returns the name // of the up-front server. func host(req *http.Request) string { if fwd := req.Header.Get("X-Forwarded-Host"); fwd != "" { return fwd } return req.Host } func passwordResetRequest( input interface{}, req *http.Request, _ *sql.Conn, ) (jr JSONResult, err error) { // We do the checks and the emailing in background // no reduce the risks of timing attacks. go func(https, host string, user *models.PWResetUser) { if err := backgroundRequest(https, host, user); err != nil { log.Printf("error: %v\n", err) } }(useHTTPS(req), host(req), input.(*models.PWResetUser)) // Send a neutral message to avoid being an user oracle. const neutralMessage = "If this account exists, a reset link will be mailed." jr.Result = &struct { Message string `json:"message"` }{neutralMessage} return } func passwordReset( _ interface{}, req *http.Request, _ *sql.Conn, ) (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 ctx := req.Context() if err = auth.RunAs( ctx, pwResetRole, func(conn *sql.Conn) error { err := conn.QueryRowContext(ctx, 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 := conn.ExecContext(ctx, 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 = conn.ExecContext(ctx, deleteRequestSQL, hash) return err }); err == nil { body := changedMessageBody(useHTTPS(req), user, password, host(req)) if err = misc.SendMail(email, "Password Reset Done", body); err == nil { jr.Result = &struct { SendTo string `json:"send-to"` }{email} } } return }