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
+}