changeset 226:63dd5216eee4

Refactored gemma server to be more REST-like.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Thu, 26 Jul 2018 12:24:30 +0200
parents 8b9cae6d3a21
children 6620b5f649f8
files 3rdpartylibs.sh auth/middleware.go cmd/gemma/root.go cmd/gemma/token.go cmd/gemma/user.go controllers/routes.go controllers/token.go controllers/user.go docs/schnittstellen.txt
diffstat 9 files changed, 321 insertions(+), 282 deletions(-) [+]
line wrap: on
line diff
--- a/3rdpartylibs.sh	Wed Jul 25 18:26:54 2018 +0200
+++ b/3rdpartylibs.sh	Thu Jul 26 12:24:30 2018 +0200
@@ -4,3 +4,4 @@
 go get -u -v github.com/mitchellh/go-homedir
 go get -u -v github.com/spf13/cobra
 go get -u -v github.com/spf13/viper
+go get -u -v github.com/gorilla/mux
--- a/auth/middleware.go	Wed Jul 25 18:26:54 2018 +0200
+++ b/auth/middleware.go	Thu Jul 26 12:24:30 2018 +0200
@@ -73,3 +73,9 @@
 		return false
 	}
 }
+
+func EnsureRole(name string) func(func(http.ResponseWriter, *http.Request)) http.Handler {
+	return func(fn func(http.ResponseWriter, *http.Request)) http.Handler {
+		return SessionMiddleware(SessionChecker(http.HandlerFunc(fn), HasRole(name)))
+	}
+}
--- a/cmd/gemma/root.go	Wed Jul 25 18:26:54 2018 +0200
+++ b/cmd/gemma/root.go	Thu Jul 26 12:24:30 2018 +0200
@@ -12,10 +12,14 @@
 
 	"gemma.intevation.de/gemma/auth"
 	"gemma.intevation.de/gemma/config"
-	homedir "github.com/mitchellh/go-homedir"
+	"gemma.intevation.de/gemma/controllers"
+
+	"github.com/gorilla/mux"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
+
+	homedir "github.com/mitchellh/go-homedir"
 )
 
 var rootCmd = &cobra.Command{
@@ -80,6 +84,15 @@
 	}
 }
 
+func prepareConnectionPool() {
+	// Install connection pool
+	cp, err := auth.NewConnectionPool(config.Config.SessionStore)
+	if err != nil {
+		log.Fatalf("Error with session store: %v\n", err)
+	}
+	auth.ConnPool = cp
+}
+
 func injectViper(cmd *cobra.Command) {
 	cmd.Flags().VisitAll(func(f *pflag.Flag) {
 		if !f.Changed {
@@ -96,30 +109,22 @@
 	// own config storage.
 	injectViper(cmd)
 
-	// Install connection pool
-	cp, err := auth.NewConnectionPool(config.Config.SessionStore)
-	if err != nil {
-		log.Fatalf("Error with session store: %v\n", err)
-	}
-	auth.ConnPool = cp
-
-	p, err := filepath.Abs(config.Config.Web)
+	web, err := filepath.Abs(config.Config.Web)
 	if err != nil {
 		log.Fatalf("error: %v\n", err)
 	}
-	mux := http.NewServeMux()
-	mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir(p))))
-	mux.HandleFunc("/api/token", token)
-	mux.Handle("/api/logout", auth.SessionMiddleware(http.HandlerFunc(token)))
-	mux.Handle("/api/renew", auth.SessionMiddleware(http.HandlerFunc(renew)))
-	mux.Handle("/api/create_user",
-		auth.SessionMiddleware(
-			auth.SessionChecker(http.HandlerFunc(createUser), auth.HasRole("sys_admin"))))
+
+	prepareConnectionPool()
+
+	m := mux.NewRouter()
+	controllers.BindRoutes(m)
+
+	m.PathPrefix("/").Handler(http.FileServer(http.Dir(web)))
 
 	addr := fmt.Sprintf("%s:%d", config.Config.WebHost, config.Config.WebPort)
 	log.Printf("listen on %s\n", addr)
 
-	server := http.Server{Addr: addr, Handler: mux}
+	server := http.Server{Addr: addr, Handler: m}
 
 	done := make(chan error)
 
--- a/cmd/gemma/token.go	Wed Jul 25 18:26:54 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"log"
-	"net/http"
-
-	"gemma.intevation.de/gemma/auth"
-)
-
-func renew(rw http.ResponseWriter, req *http.Request) {
-	token, _ := auth.GetToken(req)
-	newToken, err := auth.ConnPool.Renew(token)
-	switch {
-	case err == auth.ErrNoSuchToken:
-		http.NotFound(rw, req)
-		return
-	case err != nil:
-		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusInternalServerError)
-		return
-	}
-
-	session, _ := auth.GetSession(req)
-
-	var result = struct {
-		Token   string   `json:"token"`
-		Expires int64    `json:"expires"`
-		User    string   `json:"user"`
-		Roles   []string `json:"roles"`
-	}{
-		Token:   newToken,
-		Expires: session.ExpiresAt,
-		User:    session.User,
-		Roles:   session.Roles,
-	}
-
-	rw.Header().Set("Content-Type", "text/plain")
-	if err := json.NewEncoder(rw).Encode(&result); err != nil {
-		log.Printf("error: %v\n", err)
-	}
-}
-
-func logout(rw http.ResponseWriter, req *http.Request) {
-	token, _ := auth.GetToken(req)
-	deleted := auth.ConnPool.Delete(token)
-	if !deleted {
-		http.NotFound(rw, req)
-		return
-	}
-	rw.Header().Set("Content-Type", "text/plain")
-	fmt.Fprintln(rw, "token deleted")
-}
-
-func token(rw http.ResponseWriter, req *http.Request) {
-	user := req.FormValue("user")
-	password := req.FormValue("password")
-
-	token, session, err := auth.GenerateSession(user, password)
-
-	if err != nil {
-		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusInternalServerError)
-		return
-	}
-
-	var result = struct {
-		Token   string   `json:"token"`
-		Expires int64    `json:"expires"`
-		User    string   `json:"user"`
-		Roles   []string `json:"roles"`
-	}{
-		Token:   token,
-		Expires: session.ExpiresAt,
-		User:    session.User,
-		Roles:   session.Roles,
-	}
-
-	rw.Header().Set("Content-Type", "application/json")
-	if err := json.NewEncoder(rw).Encode(&result); err != nil {
-		log.Printf("error: %v\n", err)
-	}
-}
--- a/cmd/gemma/user.go	Wed Jul 25 18:26:54 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-package main
-
-import (
-	"database/sql"
-	"encoding/json"
-	"errors"
-	"log"
-	"net/http"
-	"regexp"
-	"strings"
-
-	"gemma.intevation.de/gemma/auth"
-	"github.com/jackc/pgx"
-)
-
-type (
-	Email   string
-	Country string
-	Role    string
-
-	BoundingBox struct {
-		X1 float64 `json:"x1"`
-		Y1 float64 `json:"y1"`
-		X2 float64 `json:"x2"`
-		Y2 float64 `json:"y2"`
-	}
-
-	User struct {
-		User     string       `json:"user"`
-		Role     Role         `json:"role"`
-		Password string       `json:"password"`
-		Email    Email        `json:"email"`
-		Country  Country      `json:"country"`
-		Extent   *BoundingBox `json:"extent"`
-	}
-)
-
-const (
-	createUserSQL       = `SELECT sys_admin.create_user($1, $2, $3, $4, NULL, $5)`
-	createUserExtentSQL = `SELECT sys_admin.create_user($1, $2, $3, $4,
-  ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)`
-)
-
-var (
-	// https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
-	emailRe = regexp.MustCompile(
-		`(?:[a-z0-9!#$%&'*+/=?^_` + "`" +
-			`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` + "`" +
-			`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]` +
-			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")` +
-			`@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?` +
-			`|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}` +
-			`(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]` +
-			`:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]` +
-			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])`)
-	errNoEmailAddress = errors.New("Not a valid email address")
-)
-
-func (e *Email) UnmarshalJSON(data []byte) error {
-	var s string
-	if err := json.Unmarshal(data, &s); err != nil {
-		return err
-	}
-	if !emailRe.MatchString(s) {
-		return errNoEmailAddress
-	}
-	*e = Email(s)
-	return nil
-}
-
-var (
-	validCountries = []string{
-		"AT", "BG", "DE", "HU", "HR",
-		"MD", "RO", "RS", "SK", "UA",
-	}
-	errNoValidCountry = errors.New("Not a valid country")
-)
-
-func (c *Country) UnmarshalJSON(data []byte) error {
-	var s string
-	if err := json.Unmarshal(data, &s); err != nil {
-		return err
-	}
-	s = strings.ToUpper(s)
-	for _, v := range validCountries {
-		if v == s {
-			*c = Country(v)
-			return nil
-		}
-	}
-	return errNoValidCountry
-}
-
-var (
-	validRoles = []string{
-		"waterway_user",
-		"waterway_admin",
-		"sys_admin",
-	}
-	errNoValidRole = errors.New("Not a valid role")
-)
-
-func (r *Role) UnmarshalJSON(data []byte) error {
-	var s string
-	if err := json.Unmarshal(data, &s); err != nil {
-		return err
-	}
-	s = strings.ToLower(s)
-	for _, v := range validRoles {
-		if v == s {
-			*r = Role(v)
-			return nil
-		}
-	}
-	return errNoValidRole
-}
-
-func createUser(rw http.ResponseWriter, req *http.Request) {
-
-	var user User
-
-	defer req.Body.Close()
-	if err := json.NewDecoder(req.Body).Decode(&user); err != nil {
-		http.Error(rw, "error: "+err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	token, _ := auth.GetToken(req)
-	err := auth.ConnPool.Do(token, func(db *sql.DB) (err error) {
-		if user.Extent == nil {
-			_, err = db.Exec(
-				createUserSQL,
-				string(user.Role),
-				user.User,
-				user.Password,
-				string(user.Country),
-				string(user.Email),
-			)
-		} else {
-			_, err = db.Exec(
-				createUserExtentSQL,
-				string(user.Role),
-				user.User,
-				user.Password,
-				string(user.Country),
-				user.Extent.X1, user.Extent.Y1,
-				user.Extent.X2, user.Extent.Y2,
-				string(user.Email),
-			)
-		}
-		return
-	})
-
-	var res struct {
-		Result  string `json:"result"`
-		Code    string `json:"code,omitempty"`
-		Message string `json:"message,omitempty"`
-	}
-
-	if err != nil {
-		if pgErr, ok := err.(pgx.PgError); ok {
-			res.Result = "failure"
-			res.Code = pgErr.Code
-			res.Message = pgErr.Message
-		} else {
-			log.Printf("err: %v\n", err)
-			http.Error(rw,
-				"error: "+err.Error(),
-				http.StatusInternalServerError)
-			return
-		}
-	} else {
-		res.Result = "success"
-	}
-
-	rw.Header().Set("Content-Type", "application/json")
-	if err := json.NewEncoder(rw).Encode(&res); err != nil {
-		log.Printf("error: %v\n", err)
-	}
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controllers/routes.go	Thu Jul 26 12:24:30 2018 +0200
@@ -0,0 +1,26 @@
+package controllers
+
+import (
+	"net/http"
+
+	"gemma.intevation.de/gemma/auth"
+
+	"github.com/gorilla/mux"
+)
+
+func BindRoutes(m *mux.Router) {
+
+	api := m.PathPrefix("/api").Subrouter()
+
+	sysAdmin := auth.EnsureRole("sys_admin")
+
+	api.Handle("/users", sysAdmin(createUser)).Methods(http.MethodPost)
+
+	api.HandleFunc("/login", token).
+		Methods(http.MethodGet, http.MethodPost)
+	api.Handle("/logout", auth.SessionMiddleware(http.HandlerFunc(token))).
+		Methods(http.MethodGet, http.MethodPost)
+	api.Handle("/renew", auth.SessionMiddleware(http.HandlerFunc(renew))).
+		Methods(http.MethodGet, http.MethodPost)
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controllers/token.go	Thu Jul 26 12:24:30 2018 +0200
@@ -0,0 +1,82 @@
+package controllers
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+
+	"gemma.intevation.de/gemma/auth"
+)
+
+func renew(rw http.ResponseWriter, req *http.Request) {
+	token, _ := auth.GetToken(req)
+	newToken, err := auth.ConnPool.Renew(token)
+	switch {
+	case err == auth.ErrNoSuchToken:
+		http.NotFound(rw, req)
+		return
+	case err != nil:
+		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusInternalServerError)
+		return
+	}
+
+	session, _ := auth.GetSession(req)
+
+	var result = struct {
+		Token   string   `json:"token"`
+		Expires int64    `json:"expires"`
+		User    string   `json:"user"`
+		Roles   []string `json:"roles"`
+	}{
+		Token:   newToken,
+		Expires: session.ExpiresAt,
+		User:    session.User,
+		Roles:   session.Roles,
+	}
+
+	rw.Header().Set("Content-Type", "text/plain")
+	if err := json.NewEncoder(rw).Encode(&result); err != nil {
+		log.Printf("error: %v\n", err)
+	}
+}
+
+func logout(rw http.ResponseWriter, req *http.Request) {
+	token, _ := auth.GetToken(req)
+	deleted := auth.ConnPool.Delete(token)
+	if !deleted {
+		http.NotFound(rw, req)
+		return
+	}
+	rw.Header().Set("Content-Type", "text/plain")
+	fmt.Fprintln(rw, "token deleted")
+}
+
+func token(rw http.ResponseWriter, req *http.Request) {
+	user := req.FormValue("user")
+	password := req.FormValue("password")
+
+	token, session, err := auth.GenerateSession(user, password)
+
+	if err != nil {
+		http.Error(rw, fmt.Sprintf("error: %v", err), http.StatusInternalServerError)
+		return
+	}
+
+	var result = struct {
+		Token   string   `json:"token"`
+		Expires int64    `json:"expires"`
+		User    string   `json:"user"`
+		Roles   []string `json:"roles"`
+	}{
+		Token:   token,
+		Expires: session.ExpiresAt,
+		User:    session.User,
+		Roles:   session.Roles,
+	}
+
+	rw.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(rw).Encode(&result); err != nil {
+		log.Printf("error: %v\n", err)
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/controllers/user.go	Thu Jul 26 12:24:30 2018 +0200
@@ -0,0 +1,180 @@
+package controllers
+
+import (
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"log"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"gemma.intevation.de/gemma/auth"
+	"github.com/jackc/pgx"
+)
+
+type (
+	Email   string
+	Country string
+	Role    string
+
+	BoundingBox struct {
+		X1 float64 `json:"x1"`
+		Y1 float64 `json:"y1"`
+		X2 float64 `json:"x2"`
+		Y2 float64 `json:"y2"`
+	}
+
+	User struct {
+		User     string       `json:"user"`
+		Role     Role         `json:"role"`
+		Password string       `json:"password"`
+		Email    Email        `json:"email"`
+		Country  Country      `json:"country"`
+		Extent   *BoundingBox `json:"extent"`
+	}
+)
+
+const (
+	createUserSQL       = `SELECT sys_admin.create_user($1, $2, $3, $4, NULL, $5)`
+	createUserExtentSQL = `SELECT sys_admin.create_user($1, $2, $3, $4,
+  ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)`
+)
+
+var (
+	// https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression
+	emailRe = regexp.MustCompile(
+		`(?:[a-z0-9!#$%&'*+/=?^_` + "`" +
+			`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` + "`" +
+			`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]` +
+			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")` +
+			`@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?` +
+			`|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}` +
+			`(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]` +
+			`:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]` +
+			`|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])`)
+	errNoEmailAddress = errors.New("Not a valid email address")
+)
+
+func (e *Email) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	if !emailRe.MatchString(s) {
+		return errNoEmailAddress
+	}
+	*e = Email(s)
+	return nil
+}
+
+var (
+	validCountries = []string{
+		"AT", "BG", "DE", "HU", "HR",
+		"MD", "RO", "RS", "SK", "UA",
+	}
+	errNoValidCountry = errors.New("Not a valid country")
+)
+
+func (c *Country) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	s = strings.ToUpper(s)
+	for _, v := range validCountries {
+		if v == s {
+			*c = Country(v)
+			return nil
+		}
+	}
+	return errNoValidCountry
+}
+
+var (
+	validRoles = []string{
+		"waterway_user",
+		"waterway_admin",
+		"sys_admin",
+	}
+	errNoValidRole = errors.New("Not a valid role")
+)
+
+func (r *Role) UnmarshalJSON(data []byte) error {
+	var s string
+	if err := json.Unmarshal(data, &s); err != nil {
+		return err
+	}
+	s = strings.ToLower(s)
+	for _, v := range validRoles {
+		if v == s {
+			*r = Role(v)
+			return nil
+		}
+	}
+	return errNoValidRole
+}
+
+func createUser(rw http.ResponseWriter, req *http.Request) {
+
+	var user User
+
+	defer req.Body.Close()
+	if err := json.NewDecoder(req.Body).Decode(&user); err != nil {
+		http.Error(rw, "error: "+err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	token, _ := auth.GetToken(req)
+	err := auth.ConnPool.Do(token, func(db *sql.DB) (err error) {
+		if user.Extent == nil {
+			_, err = db.Exec(
+				createUserSQL,
+				string(user.Role),
+				user.User,
+				user.Password,
+				string(user.Country),
+				string(user.Email),
+			)
+		} else {
+			_, err = db.Exec(
+				createUserExtentSQL,
+				string(user.Role),
+				user.User,
+				user.Password,
+				string(user.Country),
+				user.Extent.X1, user.Extent.Y1,
+				user.Extent.X2, user.Extent.Y2,
+				string(user.Email),
+			)
+		}
+		return
+	})
+
+	var res struct {
+		Result  string `json:"result"`
+		Code    string `json:"code,omitempty"`
+		Message string `json:"message,omitempty"`
+	}
+
+	if err != nil {
+		if pgErr, ok := err.(pgx.PgError); ok {
+			res.Result = "failure"
+			res.Code = pgErr.Code
+			res.Message = pgErr.Message
+		} else {
+			log.Printf("err: %v\n", err)
+			http.Error(rw,
+				"error: "+err.Error(),
+				http.StatusInternalServerError)
+			return
+		}
+	} else {
+		res.Result = "success"
+	}
+
+	rw.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(rw).Encode(&res); err != nil {
+		log.Printf("error: %v\n", err)
+	}
+}
--- a/docs/schnittstellen.txt	Wed Jul 25 18:26:54 2018 +0200
+++ b/docs/schnittstellen.txt	Thu Jul 26 12:24:30 2018 +0200
@@ -6,8 +6,9 @@
 | /api/users/{id}                  | DELETE  |                                                                   | APUC3        |                                                               204 |                                                |   |
 | /api/users/{id}                  | PATCH   | je nachdem, was geƤndert worden ist                               | APUC3        |                                       token, expires, user, roles | passwordprompt, defaultextent                  |   |
 | /api/users/{login}/passwordreset | POST    | user: login                                                       | GPUC3        |                                                          200, 500 |                                                |   |
-| /api/login                       | GET *   | user, password als Queryparameter (ist aktuell so)                | APUC1        |                                                               200 |                                                |   |
-| /api/logout                      | GET *   | ist aktuell so implementiert                                      | APUC2        |                                                               200 |                                                |   |
+| /api/login                       | GET/POST| user, password als Queryparameter (ist aktuell so)                | APUC1        |                                                               200 |                                                |   |
+| /api/logout                      | GET/POST| ist aktuell so implementiert                                      | APUC2        |                                                               200 |                                                |   |
+| /api/renew                       | GET/POST| ist aktuell so implementiert                                      | APUC2        |                                                               200 |                                                |   |
 | /api/health/hardware             | GET     |                                                                   | APUC8        |                                                               200 | Optionale Queryparameter: limit, from, from+to |   |
 | /api/health/system               | GET     |                                                                   | APUC8        |                                                               200 | Optionale Queryparameter: limit, from, from+to |   |
 | /api/health/access               | GET     |                                                                   | APUC8        |                                                               200 | Optionale Queryparameter: limit, from, from+to |   |