Mercurial > gemma
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 | |