Auth (subroutes), password.
This commit is contained in:
parent
16e742fcd7
commit
7da59ffef2
13 changed files with 116 additions and 28 deletions
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,6 +22,7 @@ var (
|
||||||
errWhileParsingCookie = errors.New("error while parsing cookie")
|
errWhileParsingCookie = errors.New("error while parsing cookie")
|
||||||
errTokenExpired = errors.New("token expired")
|
errTokenExpired = errors.New("token expired")
|
||||||
errGenericError = errors.New("generic error")
|
errGenericError = errors.New("generic error")
|
||||||
|
errAccessDenied = errors.New("insufficient privileges")
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupCerts(p string) error {
|
func SetupCerts(p string) error {
|
||||||
|
@ -102,6 +104,11 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, Error{errGenericError})
|
writeJSON(w, Error{errGenericError})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if mux.Vars(r)["genus"] != token.Claims["genus"] {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
writeJSON(w, Error{errAccessDenied})
|
||||||
|
return
|
||||||
|
}
|
||||||
hErr := h(w, r)
|
hErr := h(w, r)
|
||||||
if hErr != nil {
|
if hErr != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
|
@ -70,6 +70,8 @@ func Handler() *mux.Router {
|
||||||
m.Get(router.UpdateMeasurement).Handler(handler(serveUpdateMeasurement))
|
m.Get(router.UpdateMeasurement).Handler(handler(serveUpdateMeasurement))
|
||||||
m.Get(router.DeleteMeasurement).Handler(handler(serveDeleteMeasurement))
|
m.Get(router.DeleteMeasurement).Handler(handler(serveDeleteMeasurement))
|
||||||
|
|
||||||
|
m.Get(router.SubrouterListSpecies).Handler(authHandler(serveSubrouterSpeciesList))
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,3 +90,22 @@ func serveDeleteSpecies(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
return writeJSON(w, &models.Species{})
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -66,13 +66,14 @@ func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
auth_level, err := store.Users.Authenticate(username, password)
|
user_session, err := store.Users.Authenticate(username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t := jwt.New(jwt.GetSigningMethod("RS256"))
|
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()
|
t.Claims["exp"] = time.Now().Add(time.Minute * 1).Unix()
|
||||||
tokenString, err := t.SignedString(signKey)
|
tokenString, err := t.SignedString(signKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -87,5 +88,5 @@ func serveAuthenticateUser(w http.ResponseWriter, r *http.Request) error {
|
||||||
RawExpires: "0",
|
RawExpires: "0",
|
||||||
})
|
})
|
||||||
|
|
||||||
return writeJSON(w, auth_level)
|
return writeJSON(w, user_session)
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,14 +101,18 @@ func TestUser_Authenticate(t *testing.T) {
|
||||||
test_user := newUser()
|
test_user := newUser()
|
||||||
test_user.Username = "test_user"
|
test_user.Username = "test_user"
|
||||||
|
|
||||||
|
var user_session_want models.UserSession
|
||||||
|
|
||||||
calledAuthenticate := false
|
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
|
calledAuthenticate = true
|
||||||
auth_level := "read"
|
user_session_want.AccessLevel = "read"
|
||||||
return &auth_level, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +120,8 @@ func TestUser_Authenticate(t *testing.T) {
|
||||||
if !calledAuthenticate {
|
if !calledAuthenticate {
|
||||||
t.Error("!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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id BIGSERIAL NOT NULL,
|
id BIGSERIAL NOT NULL,
|
||||||
username CHARACTER VARYING(100) NOT NULL,
|
username CHARACTER VARYING(100) NOT NULL,
|
||||||
|
password CHARACTER VARYING(100) NOT NULL,
|
||||||
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package datastore
|
package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/thermokarst/bactdb/models"
|
"github.com/thermokarst/bactdb/models"
|
||||||
|
@ -39,8 +41,27 @@ func (s *speciesStore) List(opt *models.SpeciesListOptions) ([]*models.Species,
|
||||||
if opt == nil {
|
if opt == nil {
|
||||||
opt = &models.SpeciesListOptions{}
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package datastore
|
package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/thermokarst/bactdb/models"
|
"github.com/thermokarst/bactdb/models"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -31,7 +31,11 @@ func (s *usersStore) Create(user *models.User) (bool, error) {
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
user.CreatedAt = currentTime
|
user.CreatedAt = currentTime
|
||||||
user.UpdatedAt = 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 err := s.dbh.Insert(user); err != nil {
|
||||||
if strings.Contains(err.Error(), `violates unique constraint "username_idx"`) {
|
if strings.Contains(err.Error(), `violates unique constraint "username_idx"`) {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -52,14 +56,20 @@ func (s *usersStore) List(opt *models.UserListOptions) ([]*models.User, error) {
|
||||||
return users, nil
|
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 users []*models.User
|
||||||
|
var user_session models.UserSession
|
||||||
|
|
||||||
if err := s.dbh.Select(&users, `SELECT * FROM users WHERE username=$1;`, username); err != nil {
|
if err := s.dbh.Select(&users, `SELECT * FROM users WHERE username=$1;`, username); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
return nil, models.ErrUserNotFound
|
return nil, models.ErrUserNotFound
|
||||||
}
|
}
|
||||||
auth_level := "read"
|
if err := bcrypt.CompareHashAndPassword([]byte(users[0].Password), []byte(password)); err != nil {
|
||||||
return &auth_level, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
user_session.AccessLevel = "read"
|
||||||
|
user_session.Genus = "hymenobacter"
|
||||||
|
return &user_session, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/jmoiron/modl"
|
"github.com/jmoiron/modl"
|
||||||
"github.com/thermokarst/bactdb/models"
|
"github.com/thermokarst/bactdb/models"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func insertUser(t *testing.T, tx *modl.Transaction) *models.User {
|
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 {
|
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) {
|
func TestUsersStore_Get_db(t *testing.T) {
|
||||||
|
@ -93,14 +98,18 @@ func TestUsersStore_Authenticate_db(t *testing.T) {
|
||||||
|
|
||||||
user := insertUser(t, tx)
|
user := insertUser(t, tx)
|
||||||
|
|
||||||
|
want := &models.UserSession{
|
||||||
|
AccessLevel: "read",
|
||||||
|
Genus: "hymenobacter",
|
||||||
|
}
|
||||||
|
|
||||||
d := NewDatastore(tx)
|
d := NewDatastore(tx)
|
||||||
|
|
||||||
auth_level, err := d.Users.Authenticate(user.Username, "password")
|
user_session, err := d.Users.Authenticate(user.Username, "password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if !reflect.DeepEqual(user_session, want) {
|
||||||
if *auth_level != "read" {
|
t.Errorf("got session %+v, want %+v", user_session, want)
|
||||||
t.Errorf("expecting read, got %+v", auth_level)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@ func (s *speciesService) Create(species *Species) (bool, error) {
|
||||||
|
|
||||||
type SpeciesListOptions struct {
|
type SpeciesListOptions struct {
|
||||||
ListOptions
|
ListOptions
|
||||||
|
Genus string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
|
func (s *speciesService) List(opt *SpeciesListOptions) ([]*Species, error) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
type User struct {
|
type User struct {
|
||||||
Id int64 `json:"id,omitempty"`
|
Id int64 `json:"id,omitempty"`
|
||||||
Username string `db:"username" json:"username"`
|
Username string `db:"username" json:"username"`
|
||||||
|
Password string `db:"password" json:"-"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
|
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
|
||||||
DeletedAt NullTime `db:"deleted_at" json:"deletedAt"`
|
DeletedAt NullTime `db:"deleted_at" json:"deletedAt"`
|
||||||
|
@ -35,7 +36,12 @@ type UsersService interface {
|
||||||
Create(user *User) (created bool, err error)
|
Create(user *User) (created bool, err error)
|
||||||
|
|
||||||
// Authenticate a user, returns their access level.
|
// 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 (
|
var (
|
||||||
|
@ -113,7 +119,7 @@ func (s *usersService) List(opt *UserListOptions) ([]*User, error) {
|
||||||
return users, nil
|
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)
|
url, err := s.client.url(router.GetToken, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -124,20 +130,20 @@ func (s *usersService) Authenticate(username string, password string) (*string,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var auth_level *string
|
var user_session *UserSession
|
||||||
_, err = s.client.Do(req, &auth_level)
|
_, err = s.client.Do(req, &user_session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth_level, nil
|
return user_session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockUsersService struct {
|
type MockUsersService struct {
|
||||||
Get_ func(id int64) (*User, error)
|
Get_ func(id int64) (*User, error)
|
||||||
List_ func(opt *UserListOptions) ([]*User, error)
|
List_ func(opt *UserListOptions) ([]*User, error)
|
||||||
Create_ func(user *User) (bool, 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{}
|
var _ UsersService = &MockUsersService{}
|
||||||
|
@ -163,9 +169,9 @@ func (s *MockUsersService) List(opt *UserListOptions) ([]*User, error) {
|
||||||
return s.List_(opt)
|
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 {
|
if s.Authenticate_ == nil {
|
||||||
return nil, nil
|
return &UserSession{}, nil
|
||||||
}
|
}
|
||||||
return s.Authenticate_(username, password)
|
return s.Authenticate_(username, password)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,5 +67,9 @@ func API() *mux.Router {
|
||||||
m.Path("/measurements/{Id:.+}").Methods("PUT").Name(UpdateMeasurement)
|
m.Path("/measurements/{Id:.+}").Methods("PUT").Name(UpdateMeasurement)
|
||||||
m.Path("/measurements/{Id:.+}").Methods("DELETE").Name(DeleteMeasurement)
|
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
|
return m
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,4 +53,6 @@ const (
|
||||||
Measurements = "measurements:list"
|
Measurements = "measurements:list"
|
||||||
UpdateMeasurement = "measurements:update"
|
UpdateMeasurement = "measurements:update"
|
||||||
DeleteMeasurement = "measurements:delete"
|
DeleteMeasurement = "measurements:delete"
|
||||||
|
|
||||||
|
SubrouterListSpecies = "subrouter_species:list"
|
||||||
)
|
)
|
||||||
|
|
Reference in a new issue