Auth (subroutes), password.

This commit is contained in:
Matthew Dillon 2015-01-07 15:54:47 -09:00
parent 16e742fcd7
commit 7da59ffef2
13 changed files with 116 additions and 28 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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)
}

View file

@ -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
}

View file

@ -53,4 +53,6 @@ const (
Measurements = "measurements:list"
UpdateMeasurement = "measurements:update"
DeleteMeasurement = "measurements:delete"
SubrouterListSpecies = "subrouter_species:list"
)