From 7da59ffef2aab8db79813cef738316111aa527d1 Mon Sep 17 00:00:00 2001 From: Matthew Dillon Date: Wed, 7 Jan 2015 15:54:47 -0900 Subject: [PATCH] Auth (subroutes), password. --- api/auth.go | 7 +++++++ api/handler.go | 2 ++ api/species.go | 19 ++++++++++++++++++ api/users.go | 7 ++++--- api/users_test.go | 17 ++++++++++------ datastore/migrations/00001_AddUsers_up.sql | 1 + datastore/species.go | 23 +++++++++++++++++++++- datastore/users.go | 20 ++++++++++++++----- datastore/users_test.go | 19 +++++++++++++----- models/species.go | 1 + models/users.go | 22 +++++++++++++-------- router/api.go | 4 ++++ router/routes.go | 2 ++ 13 files changed, 116 insertions(+), 28 deletions(-) diff --git a/api/auth.go b/api/auth.go index 7e5c98c..bdcb2e0 100644 --- a/api/auth.go +++ b/api/auth.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" ) const ( @@ -21,6 +22,7 @@ var ( errWhileParsingCookie = errors.New("error while parsing cookie") errTokenExpired = errors.New("token expired") errGenericError = errors.New("generic error") + errAccessDenied = errors.New("insufficient privileges") ) func SetupCerts(p string) error { @@ -102,6 +104,11 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { writeJSON(w, Error{errGenericError}) return } + if mux.Vars(r)["genus"] != token.Claims["genus"] { + w.WriteHeader(http.StatusInternalServerError) + writeJSON(w, Error{errAccessDenied}) + return + } hErr := h(w, r) if hErr != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/api/handler.go b/api/handler.go index 2545a76..4db6631 100644 --- a/api/handler.go +++ b/api/handler.go @@ -70,6 +70,8 @@ func Handler() *mux.Router { m.Get(router.UpdateMeasurement).Handler(handler(serveUpdateMeasurement)) m.Get(router.DeleteMeasurement).Handler(handler(serveDeleteMeasurement)) + m.Get(router.SubrouterListSpecies).Handler(authHandler(serveSubrouterSpeciesList)) + return m } diff --git a/api/species.go b/api/species.go index fe0b7c3..83c3536 100644 --- a/api/species.go +++ b/api/species.go @@ -90,3 +90,22 @@ func serveDeleteSpecies(w http.ResponseWriter, r *http.Request) error { return writeJSON(w, &models.Species{}) } + +func serveSubrouterSpeciesList(w http.ResponseWriter, r *http.Request) error { + var opt models.SpeciesListOptions + if err := schemaDecoder.Decode(&opt, r.URL.Query()); err != nil { + return err + } + + opt.Genus = mux.Vars(r)["genus"] + + species, err := store.Species.List(&opt) + if err != nil { + return err + } + if species == nil { + species = []*models.Species{} + } + + return writeJSON(w, species) +} diff --git a/api/users.go b/api/users.go index bbd155e..3136d5a 100644 --- a/api/users.go +++ b/api/users.go @@ -66,13 +66,14 @@ func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) error { username := r.FormValue("username") password := r.FormValue("password") - auth_level, err := store.Users.Authenticate(username, password) + user_session, err := store.Users.Authenticate(username, password) if err != nil { return err } t := jwt.New(jwt.GetSigningMethod("RS256")) - t.Claims["AccessToken"] = auth_level + t.Claims["auth_level"] = user_session.AccessLevel + t.Claims["genus"] = user_session.Genus t.Claims["exp"] = time.Now().Add(time.Minute * 1).Unix() tokenString, err := t.SignedString(signKey) if err != nil { @@ -87,5 +88,5 @@ func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) error { RawExpires: "0", }) - return writeJSON(w, auth_level) + return writeJSON(w, user_session) } diff --git a/api/users_test.go b/api/users_test.go index 064fb6b..d145926 100644 --- a/api/users_test.go +++ b/api/users_test.go @@ -101,14 +101,18 @@ func TestUser_Authenticate(t *testing.T) { test_user := newUser() test_user.Username = "test_user" + var user_session_want models.UserSession + calledAuthenticate := false - store.Users.(*models.MockUsersService).Authenticate_ = func(username string, password string) (*string, error) { + store.Users.(*models.MockUsersService).Authenticate_ = func(username string, password string) (*models.UserSession, error) { calledAuthenticate = true - auth_level := "read" - return &auth_level, nil + user_session_want.AccessLevel = "read" + user_session_want.Genus = "hymenobacter" + + return &user_session_want, nil } - auth_level, err := apiClient.Users.Authenticate(test_user.Username, "password") + user_session, err := apiClient.Users.Authenticate(test_user.Username, "password") if err != nil { t.Fatal(err) } @@ -116,7 +120,8 @@ func TestUser_Authenticate(t *testing.T) { if !calledAuthenticate { t.Error("!calledAuthenticate") } - if *auth_level != "read" { - t.Errorf("got auth level %+v but wanted read", *auth_level) + + if !normalizeDeepEqual(user_session, &user_session_want) { + t.Errorf("got session %+v but wanted session %+v", user_session, user_session_want) } } diff --git a/datastore/migrations/00001_AddUsers_up.sql b/datastore/migrations/00001_AddUsers_up.sql index b63a3a7..82bf368 100644 --- a/datastore/migrations/00001_AddUsers_up.sql +++ b/datastore/migrations/00001_AddUsers_up.sql @@ -4,6 +4,7 @@ CREATE TABLE users ( id BIGSERIAL NOT NULL, username CHARACTER VARYING(100) NOT NULL, + password CHARACTER VARYING(100) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, diff --git a/datastore/species.go b/datastore/species.go index 54a5e24..e35e112 100644 --- a/datastore/species.go +++ b/datastore/species.go @@ -1,6 +1,8 @@ package datastore import ( + "fmt" + "strings" "time" "github.com/thermokarst/bactdb/models" @@ -39,8 +41,27 @@ func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species, if opt == nil { opt = &models.SpeciesListOptions{} } + + sql := `SELECT * FROM species` + + var conds []string + var vals []interface{} + + if opt.Genus != "" { + conds = append(conds, "genus_id = (SELECT id FROM genera WHERE lower(genus_name) = $1)") + vals = append(vals, opt.Genus) + } + + if len(conds) > 0 { + sql += " WHERE (" + strings.Join(conds, ") AND (") + ")" + } + + sql += fmt.Sprintf(" LIMIT $%v OFFSET $%v;", len(conds)+1, len(conds)+2) + vals = append(vals, opt.PerPageOrDefault()) + vals = append(vals, opt.Offset()) + var species []*models.Species - err := s.dbh.Select(&species, `SELECT * FROM species LIMIT $1 OFFSET $2;`, opt.PerPageOrDefault(), opt.Offset()) + err := s.dbh.Select(&species, sql, vals...) if err != nil { return nil, err } diff --git a/datastore/users.go b/datastore/users.go index 170ab93..743d9fc 100644 --- a/datastore/users.go +++ b/datastore/users.go @@ -1,11 +1,11 @@ package datastore import ( - "fmt" "strings" "time" "github.com/thermokarst/bactdb/models" + "golang.org/x/crypto/bcrypt" ) func init() { @@ -31,7 +31,11 @@ func (s *usersStore) Create(user *models.User) (bool, error) { currentTime := time.Now() user.CreatedAt = currentTime user.UpdatedAt = currentTime - fmt.Println(user) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10) + if err != nil { + panic(err) + } + user.Password = string(hashedPassword) if err := s.dbh.Insert(user); err != nil { if strings.Contains(err.Error(), `violates unique constraint "username_idx"`) { return false, err @@ -52,14 +56,20 @@ func (s *usersStore) List(opt *models.UserListOptions) ([]*models.User, error) { return users, nil } -func (s *usersStore) Authenticate(username string, password string) (*string, error) { +func (s *usersStore) Authenticate(username string, password string) (*models.UserSession, error) { var users []*models.User + var user_session models.UserSession + if err := s.dbh.Select(&users, `SELECT * FROM users WHERE username=$1;`, username); err != nil { return nil, err } if len(users) == 0 { return nil, models.ErrUserNotFound } - auth_level := "read" - return &auth_level, nil + if err := bcrypt.CompareHashAndPassword([]byte(users[0].Password), []byte(password)); err != nil { + return nil, err + } + user_session.AccessLevel = "read" + user_session.Genus = "hymenobacter" + return &user_session, nil } diff --git a/datastore/users_test.go b/datastore/users_test.go index 0180ca0..dd65f67 100644 --- a/datastore/users_test.go +++ b/datastore/users_test.go @@ -6,6 +6,7 @@ import ( "github.com/jmoiron/modl" "github.com/thermokarst/bactdb/models" + "golang.org/x/crypto/bcrypt" ) func insertUser(t *testing.T, tx *modl.Transaction) *models.User { @@ -20,7 +21,11 @@ func insertUser(t *testing.T, tx *modl.Transaction) *models.User { } func newUser() *models.User { - return &models.User{Username: "Test User"} + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), 10) + return &models.User{ + Username: "Test User", + Password: string(hashedPassword), + } } func TestUsersStore_Get_db(t *testing.T) { @@ -93,14 +98,18 @@ func TestUsersStore_Authenticate_db(t *testing.T) { user := insertUser(t, tx) + want := &models.UserSession{ + AccessLevel: "read", + Genus: "hymenobacter", + } + d := NewDatastore(tx) - auth_level, err := d.Users.Authenticate(user.Username, "password") + user_session, err := d.Users.Authenticate(user.Username, "password") if err != nil { t.Fatal(err) } - - if *auth_level != "read" { - t.Errorf("expecting read, got %+v", auth_level) + if !reflect.DeepEqual(user_session, want) { + t.Errorf("got session %+v, want %+v", user_session, want) } } diff --git a/models/species.go b/models/species.go index 957eaa5..c705ca8 100644 --- a/models/species.go +++ b/models/species.go @@ -93,6 +93,7 @@ func (s *speciesService) Create(species *Species) (bool, error) { type SpeciesListOptions struct { ListOptions + Genus string } func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) { diff --git a/models/users.go b/models/users.go index d9d0a4f..9e067f4 100644 --- a/models/users.go +++ b/models/users.go @@ -14,6 +14,7 @@ import ( type User struct { Id int64 `json:"id,omitempty"` Username string `db:"username" json:"username"` + Password string `db:"password" json:"-"` CreatedAt time.Time `db:"created_at" json:"createdAt"` UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` DeletedAt NullTime `db:"deleted_at" json:"deletedAt"` @@ -35,7 +36,12 @@ type UsersService interface { Create(user *User) (created bool, err error) // Authenticate a user, returns their access level. - Authenticate(username string, password string) (accessLevel *string, err error) + Authenticate(username string, password string) (user_session *UserSession, err error) +} + +type UserSession struct { + AccessLevel string `json:"access_level"` + Genus string `json:"genus"` } var ( @@ -113,7 +119,7 @@ func (s *usersService) List(opt *UserListOptions) ([]*User, error) { return users, nil } -func (s *usersService) Authenticate(username string, password string) (*string, error) { +func (s *usersService) Authenticate(username string, password string) (*UserSession, error) { url, err := s.client.url(router.GetToken, nil, nil) if err != nil { return nil, err @@ -124,20 +130,20 @@ func (s *usersService) Authenticate(username string, password string) (*string, return nil, err } - var auth_level *string - _, err = s.client.Do(req, &auth_level) + var user_session *UserSession + _, err = s.client.Do(req, &user_session) if err != nil { return nil, err } - return auth_level, nil + return user_session, nil } type MockUsersService struct { Get_ func(id int64) (*User, error) List_ func(opt *UserListOptions) ([]*User, error) Create_ func(user *User) (bool, error) - Authenticate_ func(username string, password string) (*string, error) + Authenticate_ func(username string, password string) (*UserSession, error) } var _ UsersService = &MockUsersService{} @@ -163,9 +169,9 @@ func (s *MockUsersService) List(opt *UserListOptions) ([]*User, error) { return s.List_(opt) } -func (s *MockUsersService) Authenticate(username string, password string) (*string, error) { +func (s *MockUsersService) Authenticate(username string, password string) (*UserSession, error) { if s.Authenticate_ == nil { - return nil, nil + return &UserSession{}, nil } return s.Authenticate_(username, password) } diff --git a/router/api.go b/router/api.go index 5ba67b3..37f24df 100644 --- a/router/api.go +++ b/router/api.go @@ -67,5 +67,9 @@ func API() *mux.Router { m.Path("/measurements/{Id:.+}").Methods("PUT").Name(UpdateMeasurement) m.Path("/measurements/{Id:.+}").Methods("DELETE").Name(DeleteMeasurement) + // Subrouter for auth/security + s := m.PathPrefix("/{genus}").Subrouter() + s.Path("/species").Methods("GET").Name(SubrouterListSpecies) + return m } diff --git a/router/routes.go b/router/routes.go index 0e600b6..5140620 100644 --- a/router/routes.go +++ b/router/routes.go @@ -53,4 +53,6 @@ const ( Measurements = "measurements:list" UpdateMeasurement = "measurements:update" DeleteMeasurement = "measurements:delete" + + SubrouterListSpecies = "subrouter_species:list" )