Working on securing routes and adding auth levels.

This commit is contained in:
Matthew Dillon 2014-12-18 11:14:55 -09:00
parent f912a434b5
commit ea73ecedb9
11 changed files with 141 additions and 51 deletions

View file

@ -5,7 +5,6 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"time"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
) )
@ -50,35 +49,6 @@ func init() {
} }
} }
func serveToken(w http.ResponseWriter, r *http.Request) error {
t := jwt.New(jwt.GetSigningMethod("RS256"))
// Set our claims
t.Claims["AccessToken"] = "level1"
t.Claims["CustomUserInfo"] = struct {
Name string
Kind string
}{"mrdillon", "human"}
// Set the expire time
// See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-20#section-4.1.4
t.Claims["exp"] = time.Now().Add(time.Minute * 1).Unix()
tokenString, err := t.SignedString(signKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return errWhileSigningToken
}
http.SetCookie(w, &http.Cookie{
Name: tokenName,
Value: tokenString,
Path: "/",
RawExpires: "0",
})
return writeJSON(w, Message{"success"})
}
type authHandler func(http.ResponseWriter, *http.Request) error type authHandler func(http.ResponseWriter, *http.Request) error
// Only accessible with a valid token // Only accessible with a valid token

View file

@ -20,8 +20,9 @@ func Handler() *mux.Router {
m.Get(router.User).Handler(handler(serveUser)) m.Get(router.User).Handler(handler(serveUser))
m.Get(router.CreateUser).Handler(handler(serveCreateUser)) m.Get(router.CreateUser).Handler(handler(serveCreateUser))
m.Get(router.Users).Handler(handler(serveUsers)) m.Get(router.Users).Handler(handler(serveUsers))
m.Get(router.GetToken).Handler(handler(serveAuthenticateUser))
m.Get(router.Genus).Handler(handler(serveGenus)) m.Get(router.Genus).Handler(authHandler(serveGenus))
m.Get(router.CreateGenus).Handler(handler(serveCreateGenus)) m.Get(router.CreateGenus).Handler(handler(serveCreateGenus))
m.Get(router.Genera).Handler(handler(serveGenera)) m.Get(router.Genera).Handler(handler(serveGenera))
m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus)) m.Get(router.UpdateGenus).Handler(handler(serveUpdateGenus))
@ -69,9 +70,6 @@ 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.GetToken).Handler(handler(serveToken))
m.Get(router.Restricted).Handler(authHandler(restrictedHandler))
return m return m
} }

View file

@ -4,7 +4,9 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/cookiejar"
"net/http/httptest" "net/http/httptest"
"net/url"
"github.com/thermokarst/bactdb/datastore" "github.com/thermokarst/bactdb/datastore"
"github.com/thermokarst/bactdb/models" "github.com/thermokarst/bactdb/models"
@ -16,12 +18,19 @@ func init() {
var ( var (
serveMux = http.NewServeMux() serveMux = http.NewServeMux()
httpClient = http.Client{Transport: (*muxTransport)(serveMux)} cookieJar, _ = cookiejar.New(nil)
httpClient = http.Client{
Transport: (*muxTransport)(serveMux),
Jar: cookieJar,
}
apiClient = models.NewClient(&httpClient) apiClient = models.NewClient(&httpClient)
) )
func setup() { func setup() {
store = datastore.NewMockDatastore() store = datastore.NewMockDatastore()
resp, _ := httpClient.PostForm(apiClient.BaseURL.String()+"authenticate/",
url.Values{"username": {"test_user"}, "password": {"password"}})
defer resp.Body.Close()
} }
type muxTransport http.ServeMux type muxTransport http.ServeMux

View file

@ -3,7 +3,9 @@ package api
import ( import (
"encoding/json" "encoding/json"
"strconv" "strconv"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"net/http" "net/http"
@ -59,3 +61,31 @@ func serveUsers(w http.ResponseWriter, r *http.Request) error {
return writeJSON(w, users) return writeJSON(w, users)
} }
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)
if err != nil {
return err
}
t := jwt.New(jwt.GetSigningMethod("RS256"))
t.Claims["AccessToken"] = auth_level
t.Claims["exp"] = time.Now().Add(time.Minute * 1).Unix()
tokenString, err := t.SignedString(signKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return errWhileSigningToken
}
http.SetCookie(w, &http.Cookie{
Name: tokenName,
Value: tokenString,
Path: "/",
RawExpires: "0",
})
return writeJSON(w, auth_level)
}

View file

@ -94,3 +94,29 @@ func TestUser_List(t *testing.T) {
t.Errorf("got users %+v but wanted users %+v", users, wantUsers) t.Errorf("got users %+v but wanted users %+v", users, wantUsers)
} }
} }
func TestUser_Authenticate(t *testing.T) {
setup()
test_user := newUser()
test_user.Username = "test_user"
calledAuthenticate := false
store.Users.(*models.MockUsersService).Authenticate_ = func(username string, password string) (*string, error) {
calledAuthenticate = true
auth_level := "read"
return &auth_level, nil
}
auth_level, err := apiClient.Users.Authenticate(test_user.Username, "password")
if err != nil {
t.Fatal(err)
}
if !calledAuthenticate {
t.Error("!calledAuthenticate")
}
if *auth_level != "read" {
t.Errorf("got auth level %+v but wanted read", *auth_level)
}
}

View file

@ -51,3 +51,15 @@ 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) {
var users []*models.User
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
}

View file

@ -20,7 +20,7 @@ func insertUser(t *testing.T, tx *modl.Transaction) *models.User {
} }
func newUser() *models.User { func newUser() *models.User {
return &models.User{UserName: "Test User"} return &models.User{Username: "Test User"}
} }
func TestUsersStore_Get_db(t *testing.T) { func TestUsersStore_Get_db(t *testing.T) {
@ -86,3 +86,21 @@ func TestUsersStore_List_db(t *testing.T) {
t.Errorf("got users %+v, want %+v", users, want) t.Errorf("got users %+v, want %+v", users, want)
} }
} }
func TestUsersStore_Authenticate_db(t *testing.T) {
tx, _ := DB.Begin()
defer tx.Rollback()
user := insertUser(t, tx)
d := NewDatastore(tx)
auth_level, 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)
}
}

View file

@ -10,16 +10,17 @@ import (
) )
// A User is a person that has administrative access to bactdb. // A User is a person that has administrative access to bactdb.
// Todo: add password
type User struct { type User struct {
Id int64 `json:"id,omitempty"` Id int64 `json:"id,omitempty"`
UserName string `json:"userName"` Username string `db:"username" json:"username"`
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"`
} }
func NewUser() *User { func NewUser() *User {
return &User{UserName: "Test User"} return &User{Username: "Test User"}
} }
// UsersService interacts with the user-related endpoints in bactdb's API. // UsersService interacts with the user-related endpoints in bactdb's API.
@ -32,6 +33,9 @@ type UsersService interface {
// Create a new user. The newly created user's ID is written to user.Id // Create a new user. The newly created user's ID is written to user.Id
Create(user *User) (created bool, err error) Create(user *User) (created bool, err error)
// Authenticate a user, returns their access level.
Authenticate(username string, password string) (accessLevel *string, err error)
} }
var ( var (
@ -109,10 +113,31 @@ func (s *usersService) List(opt *UserListOptions) ([]*User, error) {
return users, nil return users, nil
} }
func (s *usersService) Authenticate(username string, password string) (*string, error) {
url, err := s.client.url(router.GetToken, nil, nil)
if err != nil {
return nil, err
}
req, err := s.client.NewRequest("POST", url.String(), nil)
if err != nil {
return nil, err
}
var auth_level *string
_, err = s.client.Do(req, &auth_level)
if err != nil {
return nil, err
}
return auth_level, 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)
} }
var _ UsersService = &MockUsersService{} var _ UsersService = &MockUsersService{}
@ -137,3 +162,10 @@ 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) {
if s.Authenticate_ == nil {
return nil, nil
}
return s.Authenticate_(username, password)
}

View file

@ -54,7 +54,7 @@ func TestUsersService_Create(t *testing.T) {
mux.HandleFunc(urlPath(t, router.CreateUser, nil), func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc(urlPath(t, router.CreateUser, nil), func(w http.ResponseWriter, r *http.Request) {
called = true called = true
testMethod(t, r, "POST") testMethod(t, r, "POST")
testBody(t, r, `{"id":1,"userName":"Test User","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n") testBody(t, r, `{"id":1,"username":"Test User","createdAt":"0001-01-01T00:00:00Z","updatedAt":"0001-01-01T00:00:00Z","deletedAt":null}`+"\n")
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
writeJSON(w, want) writeJSON(w, want)

View file

@ -9,6 +9,7 @@ func API() *mux.Router {
m.Path("/users").Methods("GET").Name(Users) m.Path("/users").Methods("GET").Name(Users)
m.Path("/users").Methods("POST").Name(CreateUser) m.Path("/users").Methods("POST").Name(CreateUser)
m.Path("/users/{Id:.+}").Methods("GET").Name(User) m.Path("/users/{Id:.+}").Methods("GET").Name(User)
m.Path("/authenticate/").Methods("POST").Name(GetToken)
// Genera // Genera
m.Path("/genera").Methods("GET").Name(Genera) m.Path("/genera").Methods("GET").Name(Genera)
@ -66,9 +67,5 @@ 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)
// Authentication
m.Path("/token/").Methods("GET").Name(GetToken)
m.Path("/restricted/").Methods("GET").Name(Restricted)
return m return m
} }

View file

@ -4,6 +4,7 @@ const (
User = "users:get" User = "users:get"
CreateUser = "users:create" CreateUser = "users:create"
Users = "users:list" Users = "users:list"
GetToken = "token:get"
Genus = "genus:get" Genus = "genus:get"
CreateGenus = "genus:create" CreateGenus = "genus:create"
@ -52,7 +53,4 @@ const (
Measurements = "measurements:list" Measurements = "measurements:list"
UpdateMeasurement = "measurements:update" UpdateMeasurement = "measurements:update"
DeleteMeasurement = "measurements:delete" DeleteMeasurement = "measurements:delete"
GetToken = "token:get"
Restricted = "restricted:get"
) )