changeset 302:0777aa6de45b

Password reset. Part I
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 01 Aug 2018 12:29:55 +0200
parents 1781e5d7cb5a
children 75e32633fb96 69e291f26bbd
files 3rdpartylibs.sh auth/opendb.go auth/pool.go cmd/gemma/root.go config/config.go controllers/json.go controllers/pwreset.go controllers/routes.go controllers/types.go
diffstat 9 files changed, 199 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/3rdpartylibs.sh	Tue Jul 31 22:24:37 2018 +0200
+++ b/3rdpartylibs.sh	Wed Aug 01 12:29:55 2018 +0200
@@ -5,3 +5,4 @@
 go get -u -v github.com/spf13/cobra
 go get -u -v github.com/spf13/viper
 go get -u -v github.com/gorilla/mux
+go get -u -v gopkg.in/gomail.v2
--- a/auth/opendb.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/auth/opendb.go	Wed Aug 01 12:29:55 2018 +0200
@@ -24,7 +24,7 @@
 		dbQuote(user), dbQuote(password), sslmode)
 }
 
-func opendb(user, password string) (*sql.DB, error) {
+func OpenDB(user, password string) (*sql.DB, error) {
 	dsn := dbDSN(
 		config.Config.DBHost, config.Config.DBPort,
 		config.Config.DBName,
@@ -45,7 +45,7 @@
 WHERE oid IN (SELECT oid FROM cte) AND rolname <> current_user`
 
 func AllOtherRoles(user, password string) ([]string, error) {
-	db, err := opendb(user, password)
+	db, err := OpenDB(user, password)
 	if err != nil {
 		return nil, err
 	}
--- a/auth/pool.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/auth/pool.go	Wed Aug 01 12:29:55 2018 +0200
@@ -256,7 +256,7 @@
 		}
 
 		session := con.session
-		db, err := opendb(session.User, session.Password)
+		db, err := OpenDB(session.User, session.Password)
 		if err != nil {
 			res <- result{err: err}
 			return
--- a/cmd/gemma/root.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/cmd/gemma/root.go	Wed Aug 01 12:29:55 2018 +0200
@@ -35,27 +35,52 @@
 	cobra.OnInitialize(initConfig)
 	fl := rootCmd.PersistentFlags
 	fl().StringVarP(&configFile, "config", "c", "", "config file (default is $HOME/.gemma.toml)")
-	fl().StringVarP(&config.Config.DBHost, "dbhost", "H", "localhost", "host of the database")
-	fl().UintVarP(&config.Config.DBPort, "dbport", "P", 5432, "port of the database")
-	fl().StringVarP(&config.Config.DBName, "dbname", "d", "gemma", "name of the database")
-	fl().StringVarP(&config.Config.DBSSLMode, "dbssl", "S", "prefer", "SSL mode of the database")
+
+	cfg := &config.Config
+
+	fl().StringVarP(&cfg.DBHost, "dbhost", "H", "localhost", "host of the database")
+	fl().UintVarP(&cfg.DBPort, "dbport", "P", 5432, "port of the database")
+	fl().StringVarP(&cfg.DBName, "dbname", "d", "gemma", "name of the database")
+	fl().StringVarP(&cfg.DBSSLMode, "dbssl", "S", "prefer", "SSL mode of the database")
 
-	fl().StringVarP(&config.Config.SessionStore, "sessions", "s", "", "path to the sessions file")
+	fl().StringVarP(&cfg.SessionStore, "sessions", "s", "", "path to the sessions file")
 
-	fl().StringVarP(&config.Config.Web, "web", "w", "", "path to the web files")
-	fl().StringVarP(&config.Config.WebHost, "host", "o", "localhost", "host of the web app")
-	fl().UintVarP(&config.Config.WebPort, "port", "p", 8000, "port of the web app")
+	fl().StringVarP(&cfg.Web, "web", "w", "", "path to the web files")
+	fl().StringVarP(&cfg.WebHost, "host", "o", "localhost", "host of the web app")
+	fl().UintVarP(&cfg.WebPort, "port", "p", 8000, "port of the web app")
+
+	fl().StringVar(&cfg.ServiceUser, "service-user", "postgres", "user to do service tasks")
+	fl().StringVar(&cfg.ServicePassword, "service-password", "", "password of user to do service tasks")
 
-	viper.BindPFlag("dbhost", fl().Lookup("dbhost"))
-	viper.BindPFlag("dbport", fl().Lookup("dbport"))
-	viper.BindPFlag("dbname", fl().Lookup("dbname"))
-	viper.BindPFlag("dbssl", fl().Lookup("dbssl"))
+	fl().StringVar(&cfg.MailHost, "mail-host", "localhost", "server to send mail with")
+	fl().UintVar(&cfg.MailPort, "mail-port", 464, "port of server to send mail with")
+	fl().StringVar(&cfg.MailUser, "mail-user", "gemma", "user to send mail with")
+	fl().StringVar(&cfg.MailPassword, "mail-password", "", "password of user to send mail with")
+	fl().StringVar(&cfg.MailFrom, "mail-from", "noreplay@localhost", "from line of mails")
+	fl().StringVar(&cfg.MailHelo, "mail-helo", "localhost", "name of server to send mail from.")
+
+	vbind := func(name string) { viper.BindPFlag(name, fl().Lookup(name)) }
+
+	vbind("dbhost")
+	vbind("dbport")
+	vbind("dbname")
+	vbind("dbssl")
 
-	viper.BindPFlag("sessions", fl().Lookup("sessions"))
+	vbind("sessions")
+
+	vbind("web")
+	vbind("host")
+	vbind("port")
 
-	viper.BindPFlag("web", fl().Lookup("web"))
-	viper.BindPFlag("host", fl().Lookup("host"))
-	viper.BindPFlag("port", fl().Lookup("port"))
+	vbind("service-user")
+	vbind("service-password")
+
+	vbind("mail-host")
+	vbind("mail-port")
+	vbind("mail-user")
+	vbind("mail-password")
+	vbind("mail-from")
+	vbind("mail-helo")
 }
 
 func initConfig() {
--- a/config/config.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/config/config.go	Wed Aug 01 12:29:55 2018 +0200
@@ -13,4 +13,14 @@
 	Web     string
 	WebHost string
 	WebPort uint
+
+	ServiceUser     string
+	ServicePassword string
+
+	MailHost     string
+	MailPort     uint
+	MailUser     string
+	MailPassword string
+	MailFrom     string
+	MailHelo     string
 }
--- a/controllers/json.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/controllers/json.go	Wed Aug 01 12:29:55 2018 +0200
@@ -43,12 +43,17 @@
 		}
 	}
 
-	token, _ := auth.GetToken(req)
 	var jr JSONResult
-	err := auth.ConnPool.Do(token, func(db *sql.DB) (err error) {
-		jr, err = j.Handle(input, req, db)
-		return err
-	})
+	var err error
+
+	if token, ok := auth.GetToken(req); ok {
+		err = auth.ConnPool.Do(token, func(db *sql.DB) (err error) {
+			jr, err = j.Handle(input, req, db)
+			return err
+		})
+	} else {
+		jr, err = j.Handle(input, req, nil)
+	}
 
 	if err != nil {
 		switch e := err.(type) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controllers/pwreset.go	Wed Aug 01 12:29:55 2018 +0200
@@ -0,0 +1,125 @@
+package controllers
+
+import (
+	"database/sql"
+	"encoding/hex"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+
+	"gemma.intevation.de/gemma/auth"
+	"gemma.intevation.de/gemma/config"
+	gomail "gopkg.in/gomail.v2"
+)
+
+const (
+	userExistsSQL = `SELECT email_address
+    FROM users.list_users WHERE username = $1
+    LIMIT 1`
+)
+
+const (
+	mailTmpl = `You have requested a password change for your account on
+%s://%s
+
+Please follow this link to get to the page where you can change your password.
+
+%s://%s/#/users/passwordreset/%s
+
+The link is only valid for one hour.
+
+Best regards
+    Your service team`
+)
+
+func messageBody(https bool, hash, serverName string) string {
+
+	var proto string
+
+	if https {
+		proto = "https"
+	} else {
+		proto = "http"
+	}
+
+	return fmt.Sprintf(mailTmpl,
+		proto, serverName,
+		proto, serverName,
+		hash)
+}
+
+const hashLength = 32
+
+func generateHash() string {
+	return hex.EncodeToString(auth.GenerateRandomKey(hashLength))
+}
+
+func passwordReset(
+	input interface{},
+	req *http.Request,
+	db *sql.DB,
+) (jr JSONResult, err error) {
+
+	log.Println("passwordreset")
+
+	user := input.(*PWResetUser)
+
+	if user.User == "" {
+		err = JSONError{http.StatusBadRequest, "Invalid user name"}
+		return
+	}
+
+	cfg := &config.Config
+
+	if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil {
+		return
+	}
+	defer db.Close()
+
+	var email string
+	err = db.QueryRow(userExistsSQL, user.User).Scan(&email)
+
+	switch {
+	case err == sql.ErrNoRows:
+		err = JSONError{http.StatusNotFound, "User does not exist."}
+		return
+	case err != nil:
+		return
+	}
+
+	hash := generateHash()
+
+	serverName := req.Host
+	useHTTPS := strings.ToLower(req.URL.Scheme) == "https"
+
+	body := messageBody(useHTTPS, hash, serverName)
+
+	m := gomail.NewMessage()
+	m.SetHeader("From", cfg.MailFrom)
+	m.SetHeader("To", email)
+	m.SetHeader("Subject", "Password Reset Link")
+	m.SetBody("text/plain", body)
+
+	d := gomail.Dialer{
+		Host:      cfg.MailHost,
+		Port:      int(cfg.MailPort),
+		Username:  cfg.MailUser,
+		Password:  cfg.MailPassword,
+		LocalName: cfg.MailHelo,
+		SSL:       cfg.MailPort == 465,
+	}
+
+	if err = d.DialAndSend(m); err != nil {
+		return
+	}
+
+	// TODO: Keep hash/user for one hour or till resolved.
+
+	jr.Result = &struct {
+		SendTo string `json:"send-to"`
+	}{
+		SendTo: email,
+	}
+	return
+}
--- a/controllers/routes.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/controllers/routes.go	Wed Aug 01 12:29:55 2018 +0200
@@ -39,6 +39,11 @@
 		Handle: deleteUser,
 	})).Methods(http.MethodDelete)
 
+	api.Handle("/users/passwordreset", &JSONHandler{
+		Input:  func() interface{} { return new(PWResetUser) },
+		Handle: passwordReset,
+	}).Methods(http.MethodPost)
+
 	api.HandleFunc("/login", login).
 		Methods(http.MethodGet, http.MethodPost)
 	api.Handle("/logout", auth.SessionMiddleware(http.HandlerFunc(logout))).
--- a/controllers/types.go	Tue Jul 31 22:24:37 2018 +0200
+++ b/controllers/types.go	Wed Aug 01 12:29:55 2018 +0200
@@ -28,6 +28,10 @@
 		Country  Country      `json:"country"`
 		Extent   *BoundingBox `json:"extent"`
 	}
+
+	PWResetUser struct {
+		User string `json:"user"`
+	}
 )
 
 var (