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"
|
||||
|
||||
"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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -53,4 +53,6 @@ const (
|
|||
Measurements = "measurements:list"
|
||||
UpdateMeasurement = "measurements:update"
|
||||
DeleteMeasurement = "measurements:delete"
|
||||
|
||||
SubrouterListSpecies = "subrouter_species:list"
|
||||
)
|
||||
|
|
Reference in a new issue