update user service (#101)

- Added all required methods for a UserService
- Added authentication handlers `/api/login` and `/api/logout`
- Added authMiddleware for authentication for `/api/v1/kolide` path
- Added authorization middleware for each endoint
- Added validation middleware for validating API inputs
- Began work on logging middleware
This commit is contained in:
Victor Vrantchan
2016-09-01 00:51:38 -04:00
committed by GitHub
parent b08d5cfb54
commit 9ab09f122a
30 changed files with 1324 additions and 329 deletions
+1
View File
@@ -1,4 +1,5 @@
# binaries
kolide-devserver/kolide-devserver
kolide-ose
*.exe
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"golang.org/x/net/context"
kitlog "github.com/go-kit/kit/log"
"github.com/kolide/kolide-ose/datastore"
"github.com/kolide/kolide-ose/kitserver"
"github.com/kolide/kolide-ose/kolide"
)
// this main is temporary. testing the new MakeHandler from kitserver
func main() {
var (
httpAddr = flag.String("http.addr", ":8080", "HTTP listen address")
ctx = context.Background()
logger kitlog.Logger
)
flag.Parse()
logger = kitlog.NewLogfmtLogger(os.Stderr)
logger = kitlog.NewContext(logger).With("ts", kitlog.DefaultTimestampUTC)
ds, _ := datastore.New("mock", "")
svcConfig := kitserver.ServiceConfig{
Datastore: ds,
SessionCookieName: "KolideSession",
BcryptCost: 12,
SaltKeySize: 24,
}
svcLogger := kitlog.NewContext(logger).With("component", "service")
var svc kolide.Service
{ // temp create an admin user
svc, _ = kitserver.NewService(svcConfig)
var (
name = "admin"
username = "admin"
password = "secret"
email = "admin@kolide.co"
enabled = true
isAdmin = true
)
admin := kolide.UserPayload{
Name: &name,
Username: &username,
Password: &password,
Email: &email,
Enabled: &enabled,
Admin: &isAdmin,
}
_, err := svc.NewUser(ctx, admin)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
svc = kitserver.NewLoggingService(svc, svcLogger)
}
httpLogger := kitlog.NewContext(logger).With("component", "http")
apiHandler := kitserver.MakeHandler(ctx, svc, httpLogger)
http.Handle("/", accessControl(apiHandler))
errs := make(chan error, 2)
go func() {
logger.Log("transport", "http", "address", *httpAddr, "msg", "listening")
errs <- http.ListenAndServe(*httpAddr, nil)
}()
go func() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
errs <- fmt.Errorf("%s", <-c)
}()
logger.Log("terminated", <-errs)
}
// cors headers
func accessControl(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type")
if r.Method == "OPTIONS" {
return
}
h.ServeHTTP(w, r)
})
}
+1
View File
@@ -81,6 +81,7 @@ func New(driver, conn string, opts ...DBOption) (kolide.Datastore, error) {
sessionKeySize: opt.sessionKeySize,
sessionLifespan: opt.sessionLifespan,
users: make(map[uint]*kolide.User),
sessions: make(map[uint]*kolide.Session),
}
return ds, nil
default:
-117
View File
@@ -15,7 +15,6 @@ import (
"github.com/jinzhu/gorm"
"github.com/kolide/kolide-ose/errors"
"github.com/kolide/kolide-ose/kolide"
"github.com/spf13/viper"
)
var tables = [...]interface{}{
@@ -239,122 +238,6 @@ func (orm gormDB) FindPassswordResetByTokenAndUserID(token string, userID uint)
return reset, err
}
func (orm gormDB) validateSession(session *kolide.Session) error {
sessionLifeSpan := viper.GetFloat64("session.expiration_seconds")
if sessionLifeSpan == 0 {
return nil
}
if time.Since(session.AccessedAt).Seconds() >= sessionLifeSpan {
err := orm.DB.Delete(session).Error
if err != nil {
return err
}
return kolide.ErrSessionExpired
}
err := orm.MarkSessionAccessed(session)
if err != nil {
return err
}
return nil
}
func (orm gormDB) FindSessionByID(id uint) (*kolide.Session, error) {
session := &kolide.Session{
ID: id,
}
err := orm.DB.Where(session).First(session).Error
if err != nil {
switch err {
case gorm.ErrRecordNotFound:
return nil, kolide.ErrNoActiveSession
default:
return nil, err
}
}
err = orm.validateSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func (orm gormDB) FindSessionByKey(key string) (*kolide.Session, error) {
session := &kolide.Session{
Key: key,
}
err := orm.DB.Where(session).First(session).Error
if err != nil {
switch err {
case gorm.ErrRecordNotFound:
return nil, kolide.ErrNoActiveSession
default:
return nil, err
}
}
err = orm.validateSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func (orm gormDB) FindAllSessionsForUser(id uint) ([]*kolide.Session, error) {
var sessions []*kolide.Session
err := orm.DB.Where("user_id = ?", id).Find(&sessions).Error
return sessions, err
}
func (orm gormDB) CreateSessionForUserID(userID uint) (*kolide.Session, error) {
sessionKeySize := viper.GetInt("session.key_size")
if sessionKeySize == 0 {
sessionKeySize = 24
}
key := make([]byte, sessionKeySize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
session := kolide.Session{
UserID: userID,
Key: base64.StdEncoding.EncodeToString(key),
}
err = orm.DB.Create(&session).Error
if err != nil {
return nil, err
}
err = orm.MarkSessionAccessed(&session)
if err != nil {
return nil, err
}
return &session, nil
}
func (orm gormDB) DestroySession(session *kolide.Session) error {
return orm.DB.Delete(session).Error
}
func (orm gormDB) DestroyAllSessionsForUser(id uint) error {
return orm.DB.Delete(&kolide.Session{}, "user_id = ?", id).Error
}
func (orm gormDB) MarkSessionAccessed(session *kolide.Session) error {
session.AccessedAt = time.Now().UTC()
return orm.DB.Save(session).Error
}
func (orm gormDB) NewQuery(query *kolide.Query) error {
if query == nil {
return errors.New(
+127
View File
@@ -0,0 +1,127 @@
package datastore
import (
"crypto/rand"
"encoding/base64"
"time"
"github.com/jinzhu/gorm"
"github.com/kolide/kolide-ose/kolide"
"github.com/spf13/viper"
)
func (orm gormDB) FindSessionByID(id uint) (*kolide.Session, error) {
session := &kolide.Session{
ID: id,
}
err := orm.DB.Where(session).First(session).Error
if err != nil {
switch err {
case gorm.ErrRecordNotFound:
return nil, kolide.ErrNoActiveSession
default:
return nil, err
}
}
err = orm.validateSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func (orm gormDB) FindSessionByKey(key string) (*kolide.Session, error) {
session := &kolide.Session{
Key: key,
}
err := orm.DB.Where(session).First(session).Error
if err != nil {
switch err {
case gorm.ErrRecordNotFound:
return nil, kolide.ErrNoActiveSession
default:
return nil, err
}
}
err = orm.validateSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func (orm gormDB) FindAllSessionsForUser(id uint) ([]*kolide.Session, error) {
var sessions []*kolide.Session
err := orm.DB.Where("user_id = ?", id).Find(&sessions).Error
return sessions, err
}
func (orm gormDB) CreateSessionForUserID(userID uint) (*kolide.Session, error) {
sessionKeySize := viper.GetInt("session.key_size")
if sessionKeySize == 0 {
sessionKeySize = 24
}
key := make([]byte, sessionKeySize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
session := kolide.Session{
UserID: userID,
Key: base64.StdEncoding.EncodeToString(key),
}
err = orm.DB.Create(&session).Error
if err != nil {
return nil, err
}
err = orm.MarkSessionAccessed(&session)
if err != nil {
return nil, err
}
return &session, nil
}
func (orm gormDB) DestroySession(session *kolide.Session) error {
return orm.DB.Delete(session).Error
}
func (orm gormDB) DestroyAllSessionsForUser(id uint) error {
return orm.DB.Delete(&kolide.Session{}, "user_id = ?", id).Error
}
func (orm gormDB) MarkSessionAccessed(session *kolide.Session) error {
session.AccessedAt = time.Now().UTC()
return orm.DB.Save(session).Error
}
func (orm gormDB) validateSession(session *kolide.Session) error {
sessionLifeSpan := viper.GetFloat64("session.expiration_seconds")
if sessionLifeSpan == 0 {
return nil
}
if time.Since(session.AccessedAt).Seconds() >= sessionLifeSpan {
err := orm.DB.Delete(session).Error
if err != nil {
return err
}
return kolide.ErrSessionExpired
}
err := orm.MarkSessionAccessed(session)
if err != nil {
return err
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package datastore
import (
"sync"
"github.com/kolide/kolide-ose/kolide"
)
type mockDB struct {
kolide.Datastore
Driver string
sessionKeySize int
sessionLifespan float64
mtx sync.RWMutex
users map[uint]*kolide.User
sessions map[uint]*kolide.Session
}
func (orm *mockDB) Name() string {
return "mock"
}
func (orm *mockDB) Migrate() error {
return nil
}
func (orm *mockDB) Drop() error {
orm.mtx.Lock()
defer orm.mtx.Unlock()
orm.users = make(map[uint]*kolide.User)
orm.sessions = make(map[uint]*kolide.Session)
return nil
}
+98
View File
@@ -0,0 +1,98 @@
package datastore
import (
"crypto/rand"
"encoding/base64"
"time"
"github.com/kolide/kolide-ose/kolide"
)
func (orm *mockDB) FindSessionByKey(key string) (*kolide.Session, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
for _, session := range orm.sessions {
if session.Key == key {
return session, nil
}
}
return nil, ErrNotFound
}
func (orm *mockDB) FindSessionByID(id uint) (*kolide.Session, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
if session, ok := orm.sessions[id]; ok {
return session, nil
}
return nil, ErrNotFound
}
func (orm *mockDB) FindAllSessionsForUser(id uint) ([]*kolide.Session, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
var sessions []*kolide.Session
for _, session := range orm.sessions {
if session.UserID == id {
sessions = append(sessions, session)
}
}
if len(sessions) == 0 {
return nil, ErrNotFound
}
return sessions, nil
}
func (orm *mockDB) CreateSessionForUserID(userID uint) (*kolide.Session, error) {
orm.mtx.Lock()
defer orm.mtx.Unlock()
key := make([]byte, orm.sessionKeySize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
session := &kolide.Session{
UserID: userID,
Key: base64.StdEncoding.EncodeToString(key),
}
session.ID = uint(len(orm.sessions))
orm.sessions[session.ID] = session
if err = orm.MarkSessionAccessed(session); err != nil {
return nil, err
}
return session, nil
}
func (orm *mockDB) DestroySession(session *kolide.Session) error {
if _, ok := orm.sessions[session.ID]; !ok {
return ErrNotFound
}
delete(orm.sessions, session.ID)
return nil
}
func (orm *mockDB) DestroyAllSessionsForUser(id uint) error {
for _, session := range orm.sessions {
if session.UserID == id {
delete(orm.sessions, session.ID)
}
}
return nil
}
func (orm *mockDB) MarkSessionAccessed(session *kolide.Session) error {
session.AccessedAt = time.Now().UTC()
if _, ok := orm.sessions[session.ID]; !ok {
return ErrNotFound
}
orm.sessions[session.ID] = session
return nil
}
// TODO test session validation(expiration)
+1 -29
View File
@@ -1,34 +1,6 @@
package datastore
import (
"sync"
"github.com/kolide/kolide-ose/kolide"
)
type mockDB struct {
kolide.Datastore
Driver string
sessionKeySize int
sessionLifespan float64
mtx sync.RWMutex
users map[uint]*kolide.User
}
func (orm *mockDB) Name() string {
return "mock"
}
func (orm *mockDB) Migrate() error {
return nil
}
func (orm *mockDB) Drop() error {
orm.mtx.Lock()
defer orm.mtx.Unlock()
orm.users = make(map[uint]*kolide.User)
return nil
}
import "github.com/kolide/kolide-ose/kolide"
func (orm *mockDB) NewUser(user *kolide.User) (*kolide.User, error) {
orm.mtx.Lock()
+68
View File
@@ -0,0 +1,68 @@
package kitserver
import (
"errors"
"github.com/go-kit/kit/endpoint"
"golang.org/x/net/context"
)
var errNoContext = errors.New("no viewer context set")
func mustBeAdmin(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
vc, err := viewerContextFromContext(ctx)
if err != nil {
return nil, err
}
if !vc.IsAdmin() {
return nil, forbiddenError{message: "must be an admin"}
}
return next(ctx, request)
}
}
func canReadUser(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
vc, err := viewerContextFromContext(ctx)
if err != nil {
return nil, err
}
uid := requestUserIDFromContext(ctx)
// TODO discuss the semantics of this check
if !vc.CanPerformReadActionOnUser(uid) {
return nil, forbiddenError{message: "no read permissions on user"}
}
return next(ctx, request)
}
}
func canModifyUser(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
vc, err := viewerContextFromContext(ctx)
if err != nil {
return nil, err
}
uid := requestUserIDFromContext(ctx)
if !vc.CanPerformWriteActionOnUser(uid) {
return nil, forbiddenError{message: "no write permissions on user"}
}
return next(ctx, request)
}
}
func requestUserIDFromContext(ctx context.Context) uint {
userID, ok := ctx.Value("request-id").(uint)
if !ok {
return 0
}
return userID
}
func viewerContextFromContext(ctx context.Context) (*viewerContext, error) {
vc, ok := ctx.Value("viewerContext").(*viewerContext)
if !ok {
return nil, errNoContext
}
return vc, nil
}
+96
View File
@@ -0,0 +1,96 @@
package kitserver
import (
"testing"
"golang.org/x/net/context"
"github.com/go-kit/kit/endpoint"
"github.com/kolide/kolide-ose/datastore"
)
// TestEndpointPermissions tests that
// the endpoint.Middleware correctly grants or denies
// permissions to access or modify resources
func TestEndpointPermissions(t *testing.T) {
ctx := context.Background()
req := struct{}{}
ds, _ := datastore.New("mock", "")
createTestUsers(t, ds)
admin1, _ := ds.User("admin1")
user1, _ := ds.User("user1")
user2, _ := ds.User("user2")
user2.Enabled = false
e := endpoint.Nop // a test endpoint
var endpointTests = []struct {
endpoint endpoint.Endpoint
// who is making the request
vc *viewerContext
// what resource are we editing
requestID uint
// what error to expect
wantErr interface{}
}{
{
endpoint: mustBeAdmin(e),
wantErr: errNoContext,
},
{
endpoint: canReadUser(e),
wantErr: errNoContext,
},
{
endpoint: canModifyUser(e),
wantErr: errNoContext,
},
{
endpoint: mustBeAdmin(e),
vc: &viewerContext{user: admin1},
},
{
endpoint: mustBeAdmin(e),
vc: &viewerContext{user: user1},
wantErr: "must be an admin",
},
{
endpoint: canModifyUser(e),
vc: &viewerContext{user: admin1},
},
{
endpoint: canModifyUser(e),
vc: &viewerContext{user: user1},
wantErr: "no write permissions",
},
{
endpoint: canModifyUser(e),
vc: &viewerContext{user: user1},
requestID: admin1.ID,
wantErr: "no write permissions",
},
{
endpoint: canReadUser(e),
vc: &viewerContext{user: user1},
requestID: admin1.ID,
},
{
endpoint: canReadUser(e),
vc: &viewerContext{user: user2},
requestID: admin1.ID,
wantErr: "no read permissions",
},
}
for i, tt := range endpointTests {
if tt.vc != nil {
ctx = context.WithValue(ctx, "viewerContext", tt.vc)
}
if tt.requestID != 0 {
ctx = context.WithValue(ctx, "request-id", tt.requestID)
}
_, eerr := tt.endpoint(ctx, req)
if err := matchErr(eerr, tt.wantErr); err != nil {
t.Errorf("test id %d failed with %v", i, err)
}
}
}
+64 -9
View File
@@ -63,14 +63,10 @@ func (r getUserResponse) error() error { return r.Err }
func makeGetUserEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(getUserRequest)
// TODO call Service
// user, err := svc.NewUser(user)
var user *kolide.User
var err error
_ = req
user, err := svc.User(ctx, req.ID)
if err != nil {
return getUserResponse{Err: err}, nil
}
return getUserResponse{
ID: user.ID,
Username: user.Username,
@@ -78,11 +74,70 @@ func makeGetUserEndpoint(svc kolide.Service) endpoint.Endpoint {
Admin: user.Admin,
Enabled: user.Enabled,
NeedsPasswordReset: user.NeedsPasswordReset,
Err: err,
}, nil
}
}
type changePasswordRequest struct {
UserID uint `json:"user_id"`
CurrentPassword string `json:"current_password"`
PasswordResetToken string `json:"password_reset_token"`
NewPassword string `json:"new_password"`
}
type changePasswordResponse struct {
Err error `json:"error,omitempty"`
}
func (r changePasswordResponse) error() error { return r.Err }
func makeChangePasswordEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(changePasswordRequest)
err := svc.ChangePassword(ctx, req.UserID, req.CurrentPassword, req.NewPassword)
return changePasswordResponse{Err: err}, nil
}
}
type updateAdminRoleRequest struct {
UserID uint `json:"user_id"`
Admin bool `json:"admin"`
}
type updateAdminRoleResponse struct {
Err error `json:"error,omitempty"`
}
func (r updateAdminRoleResponse) error() error { return r.Err }
func makeUpdateAdminRoleEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateAdminRoleRequest)
err := svc.UpdateAdminRole(ctx, req.UserID, req.Admin)
return updateAdminRoleResponse{Err: err}, nil
}
}
type updateUserStatusRequest struct {
UserID uint `json:"user_id"`
Enabled bool `json:"enabled"`
CurrentPassword string `json:"current_password"`
}
type updateUserStatusResponse struct {
Err error `json:"error,omitempty"`
}
func (r updateUserStatusResponse) error() error { return r.Err }
func makeUpdateUserStatusEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(updateUserStatusRequest)
err := svc.UpdateUserStatus(ctx, req.UserID, req.CurrentPassword, req.Enabled)
return updateUserStatusResponse{Err: err}, nil
}
}
type modifyUserRequest struct {
ID uint
payload kolide.UserPayload
+93 -5
View File
@@ -2,6 +2,7 @@ package kitserver
import (
"net/http"
"strings"
"golang.org/x/net/context"
@@ -15,27 +16,114 @@ import (
// MakeHandler creates an http handler for the Kolide API
func MakeHandler(ctx context.Context, svc kolide.Service, logger kitlog.Logger) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerBefore(
setViewerContext(svc, logger),
),
kithttp.ServerErrorLogger(logger),
kithttp.ServerErrorEncoder(encodeError),
kithttp.ServerAfter(
kithttp.SetContentType("application/json; charset=utf-8"),
),
}
// make all the endpoints
// the endpoints are wrapped in middleware with correct permissions
// this is a bit simplistic, but so are the permissions
// the reason's it's not a Service interface wrapper instead:
// - the permissions are too simple to justify it. having 3-4 endpoint Middleware vs wrapping each service method individually.
// - service API is still not stable yet
var (
createUserEndpoint = mustBeAdmin(makeCreateUserEndpoint(svc))
getUserEndpoint = canReadUser(makeGetUserEndpoint(svc))
changePasswordEndpoint = canModifyUser(makeChangePasswordEndpoint(svc))
updateAdminRoleEndpoint = mustBeAdmin(makeUpdateAdminRoleEndpoint(svc))
updateUserStatusEndpoint = canModifyUser(makeUpdateUserStatusEndpoint(svc))
)
createUserHandler := kithttp.NewServer(
ctx,
makeCreateUserEndpoint(svc),
createUserEndpoint,
decodeCreateUserRequest,
encodeResponse,
opts...,
)
getUserHandler := kithttp.NewServer(
ctx,
getUserEndpoint,
decodeGetUserRequest,
encodeResponse,
opts...,
)
changePasswordHandler := kithttp.NewServer(
ctx,
changePasswordEndpoint,
decodeChangePasswordRequest,
encodeResponse,
opts...,
)
updateAdminRoleHandler := kithttp.NewServer(
ctx,
updateAdminRoleEndpoint,
decodeUpdateAdminRoleRequest,
encodeResponse,
opts...,
)
updateUserStatusHandler := kithttp.NewServer(
ctx,
updateUserStatusEndpoint,
decodeUpdateUserStatusRequest,
encodeResponse,
opts...,
)
api := mux.NewRouter()
api.Handle("/api/v1/kolide/users", createUserHandler).Methods("POST")
api.Handle("/api/v1/kolide/users/{id}", getUserHandler).Methods("GET")
api.Handle("/api/v1/kolide/users/{id}/password", changePasswordHandler).Methods("POST")
api.Handle("/api/v1/kolide/users/{id}/role", updateAdminRoleHandler).Methods("POST")
api.Handle("/api/v1/kolide/users/{id}/status", updateUserStatusHandler).Methods("POST")
r := mux.NewRouter()
r.PathPrefix("/api/v1/kolide").Handler(authMiddleware(api))
r.Handle("/login", login(svc, logger)).Methods("POST")
r.Handle("/logout", logout(svc, logger)).Methods("GET")
r.PathPrefix("/api/v1/kolide").Handler(authMiddleware(svc, logger, api))
r.Handle("/api/login", login(svc, logger)).Methods("POST")
r.Handle("/api/logout", logout(svc, logger)).Methods("GET")
return r
}
// setViewerContext updates the context with a viewerContext,
// which holds the currently logged in user
func setViewerContext(svc kolide.Service, logger kitlog.Logger) kithttp.RequestFunc {
return func(ctx context.Context, r *http.Request) context.Context {
sm := svc.NewSessionManager(ctx, nil, r)
session, err := sm.Session()
if err != nil {
logger.Log("err", err, "error-source", "setViewerContext")
return ctx
}
user, err := svc.User(ctx, session.UserID)
if err != nil {
logger.Log("err", err, "error-source", "setViewerContext")
return ctx
}
ctx = context.WithValue(ctx, "viewerContext", &viewerContext{
user: user,
})
logger.Log("msg", "viewer context set", "user", user.ID)
// get the user-id for request
if strings.Contains(r.URL.Path, "users/") {
ctx = withUserIDFromRequest(r, ctx)
}
return ctx
}
}
func withUserIDFromRequest(r *http.Request, ctx context.Context) context.Context {
uid, _ := userIDFromRequest(r)
return context.WithValue(ctx, "request-id", uid)
}
+92 -18
View File
@@ -1,6 +1,7 @@
package kitserver
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -12,16 +13,30 @@ import (
"github.com/kolide/kolide-ose/kolide"
)
func login(svc kolide.UserService, logger kitlog.Logger) http.HandlerFunc {
func login(svc kolide.Service, logger kitlog.Logger) http.HandlerFunc {
ctx := context.Background()
logger = kitlog.NewContext(logger).With("method", "login")
return func(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
http.Redirect(w, r, "/", http.StatusFound)
var loginRequest = struct {
Username *string
Password *string
}{}
if err := json.NewDecoder(r.Body).Decode(&loginRequest); err != nil {
encodeResponse(ctx, w, getUserResponse{
Err: err,
})
logger.Log("err", err)
return
}
var username, password string
{
if loginRequest.Username != nil {
username = *loginRequest.Username
}
if loginRequest.Password != nil {
password = *loginRequest.Password
}
}
// retrieve user or respond with error
user, err := svc.Authenticate(ctx, username, password)
@@ -41,7 +56,27 @@ func login(svc kolide.UserService, logger kitlog.Logger) http.HandlerFunc {
logger.Log("err", err, "user", username)
return
}
// create session here
sm := svc.NewSessionManager(ctx, w, r)
// TODO it feels awkward to create and then save the session in two steps.
// the session manager should just call Save on it's own?
if err := sm.MakeSessionForUserID(user.ID); err != nil {
encodeResponse(ctx, w, getUserResponse{
Err: errors.New("error creating new user session"),
})
logger.Log("err", err, "user", username)
return
}
if err := sm.Save(); err != nil {
encodeResponse(ctx, w, getUserResponse{
Err: errors.New("error saving new user session"),
})
logger.Log("err", err, "user", username)
return
}
encodeResponse(ctx, w, getUserResponse{
ID: user.ID,
@@ -53,33 +88,60 @@ func login(svc kolide.UserService, logger kitlog.Logger) http.HandlerFunc {
})
}
}
const noAuthRedirect = "/"
func logout(svc kolide.UserService, logger kitlog.Logger) http.HandlerFunc {
func logout(svc kolide.Service, logger kitlog.Logger) http.HandlerFunc {
logger = kitlog.NewContext(logger).With("method", "logout")
ctx := context.Background()
return func(w http.ResponseWriter, r *http.Request) {
// delete session first
var username string
var user kolide.User
// TODO
sm := svc.NewSessionManager(ctx, w, r)
if err := sm.Destroy(); err != nil {
encodeResponse(ctx, w, getUserResponse{
Err: errors.New("error deleting session"),
})
logger.Log("err", err)
return
}
// redirect
http.Redirect(w, r, noAuthRedirect, http.StatusFound)
logger.Log("msg", "loggedout", "user", username, "id", user.ID)
}
}
func authMiddleware(next http.Handler) http.Handler {
func authMiddleware(svc kolide.Service, logger kitlog.Logger, next http.Handler) http.Handler {
logger = kitlog.NewContext(logger).With("method", "authMiddleware")
ctx := context.Background()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sm := svc.NewSessionManager(ctx, w, r)
session, err := sm.Session()
if err != nil {
http.Error(w,
"failed to retrieve user session. is there a user logged in?",
http.StatusUnauthorized)
logger.Log("err", err)
return
}
user, err := svc.User(ctx, session.UserID)
if err != nil {
http.Error(w,
"failed to get user from db", http.StatusUnauthorized)
logger.Log("err", err, "user", session.UserID)
return
}
if !user.Enabled {
http.Error(w, "user disabled", http.StatusUnauthorized)
}
// all good to pass
next.ServeHTTP(w, r)
})
}
// authentication error
type authError struct {
message string
}
@@ -91,7 +153,19 @@ func (e authError) Error() string {
return fmt.Sprintf("unauthorized: %s", e.message)
}
// viewerContext is a struct which represents the ability for an execution
// forbidden, set when user is authenticated, but not allowd to perform action
type forbiddenError struct {
message string
}
func (e forbiddenError) Error() string {
if e.message == "" {
return "unauthorized"
}
return fmt.Sprintf("unauthorized: %s", e.message)
}
// ViewerContext is a struct which represents the ability for an execution
// context to participate in certain actions. Most often, a ViewerContext is
// associated with an application user, but a ViewerContext can represent a
// variety of other execution contexts as well (script, test, etc). The main
@@ -133,14 +207,14 @@ func (vc *viewerContext) CanPerformActions() bool {
// CanPerformReadActionsOnUser returns a bool indicating the current user's
// ability to perform read actions on the given user
func (vc *viewerContext) CanPerformReadActionOnUser(u *kolide.User) bool {
return vc.CanPerformActions() || (vc.IsLoggedIn() && vc.IsUserID(u.ID))
func (vc *viewerContext) CanPerformReadActionOnUser(uid uint) bool {
return vc.CanPerformActions() || (vc.IsLoggedIn() && vc.IsUserID(uid))
}
// CanPerformWriteActionOnUser returns a bool indicating the current user's
// ability to perform write actions on the given user
func (vc *viewerContext) CanPerformWriteActionOnUser(u *kolide.User) bool {
return vc.CanPerformActions() && (vc.IsUserID(u.ID) || vc.IsAdmin())
func (vc *viewerContext) CanPerformWriteActionOnUser(uid uint) bool {
return vc.CanPerformActions() && (vc.IsUserID(uid) || vc.IsAdmin())
}
// IsUserID returns true if the given user id the same as the user which is
+98 -18
View File
@@ -1,12 +1,13 @@
package kitserver
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"golang.org/x/net/context"
@@ -14,22 +15,22 @@ import (
kitlog "github.com/go-kit/kit/log"
"github.com/kolide/kolide-ose/datastore"
"github.com/kolide/kolide-ose/kolide"
"github.com/stretchr/testify/assert"
)
func TestLogin(t *testing.T) {
ds, _ := datastore.New("mock", "")
svc, _ := NewService(ds)
createTestUsers(t, svc)
svc, _ := NewService(testConfig(ds))
createTestUsers(t, ds)
r := http.NewServeMux()
r.Handle("/logout", logout(svc, kitlog.NewNopLogger()))
r.Handle("/login", login(svc, kitlog.NewNopLogger()))
r.Handle("/api/logout", logout(svc, kitlog.NewNopLogger()))
r.Handle("/api/login", login(svc, kitlog.NewNopLogger()))
r.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "index")
}))
server := httptest.NewServer(r)
var loginTests = []struct {
username string
status int
@@ -62,16 +63,29 @@ func TestLogin(t *testing.T) {
Admin: boolPtr(true),
}
}
v := url.Values{}
{
v.Set("username", tt.username)
v.Set("password", tt.password)
}
resp, err := http.PostForm(server.URL+"/login", v)
if err != nil {
// test sessions
testUser, err := ds.User(tt.username)
if err != nil && err != datastore.ErrNotFound {
t.Fatal(err)
}
var loginRequest = struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: tt.username,
Password: tt.password,
}
j, err := json.Marshal(&loginRequest)
if err != nil {
t.Fatal(err)
}
requestBody := &nopCloser{bytes.NewBuffer(j)}
resp, err := http.Post(server.URL+"/api/login", "application/json", requestBody)
if err != nil {
t.Fatal(err)
}
if have, want := resp.StatusCode, tt.status; have != want {
t.Errorf("have %d, want %d", have, want)
}
@@ -102,6 +116,39 @@ func TestLogin(t *testing.T) {
t.Errorf("have %v, want %v", have, want)
}
// ensure that a non-empty cookie was in-fact set
cookie := resp.Header.Get("Set-Cookie")
assert.NotEmpty(t, cookie)
// ensure that a session was created for our test user and stored
sessions, err := ds.FindAllSessionsForUser(testUser.ID)
assert.Nil(t, err)
assert.Len(t, sessions, 1)
// ensure the session key is not blank
assert.NotEqual(t, "", sessions[0].Key)
// test logout
req, _ := http.NewRequest("GET", server.URL+"/api/logout", nil)
req.Header.Set("Cookie", cookie)
client := &http.Client{}
resp, err = client.Do(req)
if err != nil {
t.Fatal(err)
}
if have, want := resp.StatusCode, http.StatusOK; have != want {
t.Errorf("have %d, want %d", have, want)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if have, want := string(body), "index"; have != want {
t.Errorf("have %q, want %q", have, want)
}
// ensure that our user's session was deleted from the store
sessions, err = ds.FindAllSessionsForUser(testUser.ID)
assert.Len(t, sessions, 0)
}
var unauthenticated = []struct {
@@ -114,10 +161,11 @@ func TestLogin(t *testing.T) {
endpoint: "/login",
bodyType: "application/x-www-form-urlencoded",
},
{
method: "GET",
endpoint: "/logout",
},
// @groob TODO need to test logout with a cookie set
// {
// method: "GET",
// endpoint: "/logout",
// },
}
for _, tt := range unauthenticated {
@@ -141,7 +189,8 @@ func TestLogin(t *testing.T) {
}
}
func createTestUsers(t *testing.T, svc kolide.UserService) {
func createTestUsers(t *testing.T, ds kolide.Datastore) {
svc := svcWithNoValidation(testConfig(ds))
ctx := context.Background()
for _, tt := range testUsers {
payload := kolide.UserPayload{
@@ -157,3 +206,34 @@ func createTestUsers(t *testing.T, svc kolide.UserService) {
}
}
}
func svcWithNoValidation(config ServiceConfig) kolide.Service {
var svc kolide.Service
svc = service{
ds: config.Datastore,
logger: config.Logger,
saltKeySize: config.SaltKeySize,
bcryptCost: config.BcryptCost,
jwtKey: config.JWTKey,
cookieName: config.SessionCookieName,
}
return svc
}
func testConfig(ds kolide.Datastore) ServiceConfig {
return ServiceConfig{
Datastore: ds,
Logger: kitlog.NewNopLogger(),
BcryptCost: 12,
SaltKeySize: 24,
SessionCookieName: "KolideSession",
}
}
// an io.ReadCloser for new request body
type nopCloser struct {
io.Reader
}
func (nopCloser) Close() error { return nil }
+17
View File
@@ -0,0 +1,17 @@
package kitserver
import (
kitlog "github.com/go-kit/kit/log"
"github.com/kolide/kolide-ose/kolide"
)
// logging middleware logs the service actions
type loggingMiddleware struct {
kolide.Service
logger kitlog.Logger
}
// NewLoggingService takes an existing service and adds a logging wrapper
func NewLoggingService(svc kolide.Service, logger kitlog.Logger) kolide.Service {
return loggingMiddleware{Service: svc, logger: logger}
}
+75
View File
@@ -0,0 +1,75 @@
package kitserver
import (
"time"
"golang.org/x/net/context"
"github.com/kolide/kolide-ose/kolide"
)
func (mw loggingMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (user *kolide.User, err error) {
vc, err := viewerContextFromContext(ctx)
if err != nil {
return nil, err
}
var username = "none"
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "NewUser",
"user", username,
"created_by", vc.user.Username,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
user, err = mw.Service.NewUser(ctx, p)
if user != nil {
username = user.Username
}
return
}
func (mw loggingMiddleware) User(ctx context.Context, id uint) (user *kolide.User, err error) {
var username = "none"
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "User",
"user", username,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
user, err = mw.Service.User(ctx, id)
if user != nil {
username = user.Username
}
return
}
func (mw loggingMiddleware) ChangePassword(ctx context.Context, userID uint, old, new string) (err error) {
vc, err := viewerContextFromContext(ctx)
if err != nil {
return err
}
defer func(begin time.Time) {
_ = mw.logger.Log(
"method", "ChangePassword",
"user_id", userID,
"modified_by", vc.user.Username,
"err", err,
"took", time.Since(begin),
)
}(time.Now())
err = mw.Service.ChangePassword(ctx, userID, old, new)
return
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package kitserver
import (
"fmt"
"strings"
)
// matchErr is a test helper that verifies that an error is matched with an expected effor
// source:
// https://github.com/golang/go/blob/ffa2bd27a47ef16e4d6a404dd15781ed5ba21e5d/src/net/http/response_test.go#L865
// wantErr can be nil, an error value to match exactly, or type string to
// match a substring.
func matchErr(err error, wantErr interface{}) error {
if err == nil {
if wantErr == nil {
return nil
}
if sub, ok := wantErr.(string); ok {
return fmt.Errorf("unexpected success; want error with substring %q", sub)
}
return fmt.Errorf("unexpected success; want error %v", wantErr)
}
if wantErr == nil {
return fmt.Errorf("%v; want success", err)
}
if sub, ok := wantErr.(string); ok {
if strings.Contains(err.Error(), sub) {
return nil
}
return fmt.Errorf("error = %v; want an error with substring %q", err, sub)
}
if err == wantErr {
return nil
}
return fmt.Errorf("%v; want %v", err, wantErr)
}
+51 -12
View File
@@ -1,26 +1,65 @@
// Package kitserver holds the implementation of the kolide service interface and the HTTP endpoints
// for the API
package kitserver
import "github.com/kolide/kolide-ose/kolide"
// configuration defaults
const (
defaultBcryptCost int = 12
defaultSaltKeySize int = 24
import (
kitlog "github.com/go-kit/kit/log"
"github.com/kolide/kolide-ose/kolide"
)
func NewService(ds kolide.Datastore) (kolide.Service, error) {
// configuration defaults
// TODO move to main?
const (
defaultBcryptCost int = 12
defaultSaltKeySize int = 24
defaultCookieName string = "KolideSession"
)
// NewService creates a new service from the config struct
func NewService(config ServiceConfig) (kolide.Service, error) {
var svc kolide.Service
svc = service{
bcryptCost: defaultBcryptCost,
saltKeySize: defaultSaltKeySize,
ds: ds,
ds: config.Datastore,
logger: config.Logger,
saltKeySize: config.SaltKeySize,
bcryptCost: config.BcryptCost,
jwtKey: config.JWTKey,
cookieName: config.SessionCookieName,
OsqueryEnrollSecret: config.OsqueryEnrollSecret,
OsqueryNodeKeySize: config.OsqueryNodeKeySize,
}
svc = validationMiddleware{svc}
return svc, nil
}
type service struct {
bcryptCost int
ds kolide.Datastore
logger kitlog.Logger
saltKeySize int
ds kolide.Datastore
bcryptCost int
jwtKey string
cookieName string
OsqueryEnrollSecret string
OsqueryNodeKeySize int
}
// ServiceConfig holds the parameters for creating a Service
type ServiceConfig struct {
Datastore kolide.Datastore
Logger kitlog.Logger
// password config
SaltKeySize int
BcryptCost int
// session config
JWTKey string
SessionCookieName string
// osquery config
OsqueryEnrollSecret string
OsqueryNodeKeySize int
}
+45
View File
@@ -0,0 +1,45 @@
package kitserver
import (
"fmt"
"net/http"
"golang.org/x/net/context"
"github.com/kolide/kolide-ose/datastore"
"github.com/kolide/kolide-ose/kolide"
)
func (svc service) Authenticate(ctx context.Context, username, password string) (*kolide.User, error) {
user, err := svc.ds.User(username)
switch err {
case nil:
case datastore.ErrNotFound:
return nil, authError{
message: fmt.Sprintf("user %s not found", username),
}
default:
return nil, err
}
if !user.Enabled {
return nil, authError{
message: fmt.Sprintf("account disabled %s", username),
}
}
if err := user.ValidatePassword(password); err != nil {
return nil, authError{
message: fmt.Sprintf("invalid password for user %s", username),
}
}
return user, nil
}
func (svc service) NewSessionManager(ctx context.Context, w http.ResponseWriter, r *http.Request) *kolide.SessionManager {
return &kolide.SessionManager{
Request: r,
Writer: w,
Store: svc.ds,
JWTKey: svc.jwtKey,
CookieName: svc.cookieName,
}
}
+19 -43
View File
@@ -5,92 +5,67 @@ import (
"encoding/base64"
"fmt"
"github.com/kolide/kolide-ose/datastore"
"github.com/kolide/kolide-ose/kolide"
"golang.org/x/crypto/bcrypt"
"golang.org/x/net/context"
)
func (s service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
user, err := userFromPayload(p, s.saltKeySize, s.bcryptCost)
func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
user, err := userFromPayload(p, svc.saltKeySize, svc.bcryptCost)
if err != nil {
return nil, err
}
user, err = s.ds.NewUser(user)
user, err = svc.ds.NewUser(user)
if err != nil {
return nil, err
}
return user, nil
}
func (s service) User(ctx context.Context, id uint) (*kolide.User, error) {
// TODO: @groob
// a user is loaded for almost every request...
// consider loading the user from an in memory cache for most read operations
// and possibly only query the DB if the user is being queried for a write operation
// could be a calling context
return s.ds.UserByID(id)
func (svc service) User(ctx context.Context, id uint) (*kolide.User, error) {
return svc.ds.UserByID(id)
}
func (s service) Authenticate(ctx context.Context, username, password string) (*kolide.User, error) {
user, err := s.ds.User(username)
switch err {
case nil:
case datastore.ErrNotFound:
return nil, authError{
message: fmt.Sprintf("user %s not found", username),
}
default:
return nil, err
}
if err := user.ValidatePassword(password); err != nil {
return nil, authError{
message: fmt.Sprintf("unauthorized: invalid password for user %s", username),
}
}
return user, nil
}
func (s service) ChangePassword(ctx context.Context, userID uint, old, new string) error {
user, err := s.User(ctx, userID)
func (svc service) ChangePassword(ctx context.Context, userID uint, old, new string) error {
user, err := svc.User(ctx, userID)
if err != nil {
return err
}
if err := user.ValidatePassword(old); err != nil {
return fmt.Errorf("old password validation failed: %v", err)
return fmt.Errorf("current password validation failed: %v", err)
}
hashed, salt, err := hashPassword(new, s.saltKeySize, s.bcryptCost)
hashed, salt, err := hashPassword(new, svc.saltKeySize, svc.bcryptCost)
if err != nil {
return err
}
user.Salt = salt
user.Password = hashed
return s.saveUser(user)
return svc.saveUser(user)
}
func (s service) UpdateAdminRole(ctx context.Context, userID uint, isAdmin bool) error {
user, err := s.User(ctx, userID)
func (svc service) UpdateAdminRole(ctx context.Context, userID uint, isAdmin bool) error {
user, err := svc.User(ctx, userID)
if err != nil {
return err
}
user.Admin = isAdmin
return s.saveUser(user)
return svc.saveUser(user)
}
func (s service) UpdateStatus(ctx context.Context, userID uint, enabled bool) error {
user, err := s.User(ctx, userID)
func (svc service) UpdateUserStatus(ctx context.Context, userID uint, password string, enabled bool) error {
user, err := svc.User(ctx, userID)
if err != nil {
return err
}
user.Enabled = enabled
return s.saveUser(user)
return svc.saveUser(user)
}
// saves user in datastore.
// doesn't need to be exposed to the transport
// the service should expose actions for modifying a user instead
func (s service) saveUser(user *kolide.User) error {
return s.ds.SaveUser(user)
func (svc service) saveUser(user *kolide.User) error {
return svc.ds.SaveUser(user)
}
func userFromPayload(p kolide.UserPayload, keySize, cost int) (*kolide.User, error) {
@@ -105,6 +80,7 @@ func userFromPayload(p kolide.UserPayload, keySize, cost int) (*kolide.User, err
Admin: falseIfNil(p.Admin),
NeedsPasswordReset: falseIfNil(p.NeedsPasswordReset),
Salt: salt,
Enabled: true,
Password: hashed,
}, nil
}
+15 -9
View File
@@ -11,8 +11,9 @@ import (
func TestCreateUser(t *testing.T) {
ds, _ := datastore.New("mock", "")
svc, _ := NewService(ds)
svc, _ := NewService(testConfig(ds))
ctx := context.Background()
var createUserTests = []struct {
Username *string
Password *string
@@ -24,7 +25,7 @@ func TestCreateUser(t *testing.T) {
{
Username: stringPtr("admin1"),
Password: stringPtr("foobar"),
Err: errInvalidArgument,
Err: invalidArgumentError{},
},
{
Username: stringPtr("admin1"),
@@ -35,7 +36,6 @@ func TestCreateUser(t *testing.T) {
},
}
ctx := context.Background()
for _, tt := range createUserTests {
payload := kolide.UserPayload{
Username: tt.Username,
@@ -45,11 +45,12 @@ func TestCreateUser(t *testing.T) {
NeedsPasswordReset: tt.NeedsPasswordReset,
}
user, err := svc.NewUser(ctx, payload)
if err != nil {
if err != tt.Err {
t.Fatalf("got %q, want %q", err, tt.Err)
}
switch err.(type) {
case nil:
case invalidArgumentError:
continue
default:
t.Fatalf("got %q, want %q", err, tt.Err)
}
if user.ID == 0 {
@@ -86,8 +87,8 @@ func TestCreateUser(t *testing.T) {
func TestChangeUserPassword(t *testing.T) {
ds, _ := datastore.New("mock", "")
svc, _ := NewService(ds)
createTestUsers(t, svc)
svc, _ := NewService(testConfig(ds))
createTestUsers(t, ds)
var passwordChangeTests = []struct {
username string
@@ -129,6 +130,11 @@ var testUsers = map[string]kolide.UserPayload{
Password: stringPtr("foobar"),
Email: stringPtr("user1@example.com"),
},
"user2": {
Username: stringPtr("user2"),
Password: stringPtr("bazfoo"),
Email: stringPtr("user2@example.com"),
},
}
func stringPtr(s string) *string {
+21 -5
View File
@@ -3,6 +3,7 @@ package kitserver
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/kolide/kolide-ose/datastore"
@@ -10,13 +11,24 @@ import (
)
var (
// errInvalidArgument is returned when one or more arguments are invalid.
errInvalidArgument = errors.New("invalid argument")
// errBadRoute is used for mux errors
errBadRoute = errors.New("bad route")
)
type invalidArgumentError struct {
field string
required bool
}
// invalidArgumentError is returned when one or more arguments are invalid.
func (e invalidArgumentError) Error() string {
req := "optional"
if e.required {
req = "required"
}
return fmt.Sprintf("%s argument invalid or missing: %s", req, e.field)
}
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
if e, ok := response.(errorer); ok && e.error() != nil {
encodeError(ctx, e.error(), w)
@@ -37,8 +49,6 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.WriteHeader(http.StatusNotFound)
case datastore.ErrExists:
w.WriteHeader(http.StatusConflict)
case errInvalidArgument:
w.WriteHeader(http.StatusBadRequest)
default:
w.WriteHeader(typeErrsStatus(err))
}
@@ -47,10 +57,16 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
})
}
const unprocessableEntity int = 422
func typeErrsStatus(err error) int {
switch err.(type) {
case invalidArgumentError:
return unprocessableEntity
case authError:
return http.StatusUnauthorized
case forbiddenError:
return http.StatusForbidden
default:
return http.StatusInternalServerError
}
+55 -15
View File
@@ -10,7 +10,7 @@ import (
"golang.org/x/net/context"
)
func decodeCreateUserRequest(_ context.Context, r *http.Request) (interface{}, error) {
func decodeCreateUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req createUserRequest
if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil {
return nil, err
@@ -19,35 +19,75 @@ func decodeCreateUserRequest(_ context.Context, r *http.Request) (interface{}, e
return req, nil
}
func decodeGetUserRequest(_ context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
id, ok := vars["id"]
if !ok {
return nil, errBadRoute
}
uid, err := strconv.Atoi(id)
func decodeGetUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
uid, err := userIDFromRequest(r)
if err != nil {
return nil, err
}
return getUserRequest{ID: uint(uid)}, nil
return getUserRequest{ID: uid}, nil
}
func decodeModifyUserRequest(_ context.Context, r *http.Request) (interface{}, error) {
func decodeChangePasswordRequest(ctx context.Context, r *http.Request) (interface{}, error) {
uid, err := userIDFromRequest(r)
if err != nil {
return nil, err
}
var req changePasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
req.UserID = uid
return req, nil
}
func decodeUpdateAdminRoleRequest(ctx context.Context, r *http.Request) (interface{}, error) {
uid, err := userIDFromRequest(r)
if err != nil {
return nil, err
}
var req updateAdminRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
req.UserID = uid
return req, nil
}
func decodeUpdateUserStatusRequest(ctx context.Context, r *http.Request) (interface{}, error) {
uid, err := userIDFromRequest(r)
if err != nil {
return nil, err
}
var req updateUserStatusRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
req.UserID = uid
return req, nil
}
func decodeModifyUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
uid, err := userIDFromRequest(r)
if err != nil {
return nil, err
}
var req modifyUserRequest
if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil {
return nil, err
}
req.ID = uid
return req, nil
}
func userIDFromRequest(r *http.Request) (uint, error) {
vars := mux.Vars(r)
id, ok := vars["id"]
if !ok {
return nil, errBadRoute
return 0, errBadRoute
}
uid, err := strconv.Atoi(id)
if err != nil {
return nil, err
return 0, err
}
req.ID = uint(uid)
return req, nil
return uint(uid), nil
}
-28
View File
@@ -1,28 +0,0 @@
package kitserver
import (
"golang.org/x/net/context"
"github.com/kolide/kolide-ose/kolide"
)
type validationMiddleware struct {
kolide.Service
}
func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
// check required params
if p.Username == nil {
return nil, errInvalidArgument
}
if p.Password == nil {
return nil, errInvalidArgument
}
if p.Email == nil {
return nil, errInvalidArgument
}
return mw.Service.NewUser(ctx, p)
}
+49
View File
@@ -0,0 +1,49 @@
package kitserver
import (
"golang.org/x/net/context"
"github.com/kolide/kolide-ose/kolide"
)
type validationMiddleware struct {
kolide.Service
}
func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
// check required params
if p.Username == nil {
return nil, invalidArgumentError{field: "username", required: true}
}
if p.Password == nil {
return nil, invalidArgumentError{field: "password", required: true}
}
if p.Email == nil {
return nil, invalidArgumentError{field: "email", required: true}
}
return mw.Service.NewUser(ctx, p)
}
func (mw validationMiddleware) ChangePassword(ctx context.Context, userID uint, old, new string) error {
if old == "" || new == "" {
return invalidArgumentError{field: "password", required: true}
}
return mw.Service.ChangePassword(ctx, userID, old, new)
}
func (mw validationMiddleware) UpdateUserStatus(ctx context.Context, userID uint, password string, enabled bool) error {
// validate password if user is disabling self
vc, err := viewerContextFromContext(ctx)
if err != nil {
return err
}
if vc.IsUserID(userID) {
if err := vc.user.ValidatePassword(password); err != nil {
return err
}
}
return mw.Service.UpdateUserStatus(ctx, userID, password, enabled)
}
+12 -1
View File
@@ -1,15 +1,26 @@
package kolide
import "golang.org/x/net/context"
import (
"net/http"
"golang.org/x/net/context"
)
// service a interface stub
type Service interface {
UserService
AuthService
}
type UserService interface {
NewUser(ctx context.Context, p UserPayload) (*User, error)
User(ctx context.Context, id uint) (*User, error)
ChangePassword(ctx context.Context, userID uint, old, new string) error
UpdateAdminRole(ctx context.Context, userID uint, isAdmin bool) error
UpdateUserStatus(ctx context.Context, userID uint, password string, enabled bool) error
}
type AuthService interface {
Authenticate(ctx context.Context, username, password string) (*User, error)
NewSessionManager(ctx context.Context, w http.ResponseWriter, r *http.Request) *SessionManager
}
+14 -13
View File
@@ -7,7 +7,6 @@ import (
"github.com/Sirupsen/logrus"
jwt "github.com/dgrijalva/jwt-go"
"github.com/kolide/kolide-ose/errors"
"github.com/spf13/viper"
)
const publicErrorMessage string = "Session error"
@@ -73,15 +72,17 @@ type Session struct {
// SessionManager is a management object which helps with the administration of
// sessions within the application. Use NewSessionManager to create an instance
type SessionManager struct {
Store SessionStore
Request *http.Request
Writer http.ResponseWriter
session *Session
Store SessionStore
Request *http.Request
Writer http.ResponseWriter
session *Session
CookieName string
JWTKey string
}
func (sm *SessionManager) Session() (*Session, error) {
if sm.session == nil {
cookie, err := sm.Request.Cookie(viper.GetString("session.cookie_name"))
cookie, err := sm.Request.Cookie(sm.CookieName)
if err != nil {
switch err {
case http.ErrNoCookie:
@@ -94,7 +95,7 @@ func (sm *SessionManager) Session() (*Session, error) {
}
}
token, err := ParseJWT(cookie.Value)
token, err := ParseJWT(cookie.Value, sm.JWTKey)
if err != nil {
logrus.Errorf("Couldn't parse JWT token string from cookie: %s", err.Error())
return nil, ErrSessionMalformed
@@ -155,14 +156,14 @@ func (sm *SessionManager) Save() error {
var token string
var err error
if sm.session != nil {
token, err = GenerateJWT(sm.session.Key)
token, err = GenerateJWT(sm.session.Key, sm.JWTKey)
if err != nil {
return err
}
}
// TODO: set proper flags on cookie for maximum security
cookieName := viper.GetString("session.cookie_name")
cookieName := sm.CookieName
if cookieName == "" {
cookieName = "KolideSession"
}
@@ -201,22 +202,22 @@ func (sm *SessionManager) Destroy() error {
////////////////////////////////////////////////////////////////////////////////
// Given a session key create a JWT to be delivered to the client
func GenerateJWT(sessionKey string) (string, error) {
func GenerateJWT(sessionKey, jwtKey string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"session_key": sessionKey,
})
return token.SignedString([]byte(viper.GetString("auth.jwt_key")))
return token.SignedString([]byte(jwtKey))
}
// ParseJWT attempts to parse a JWT token in serialized string form into a
// JWT token in a deserialized jwt.Token struct.
func ParseJWT(token string) (*jwt.Token, error) {
func ParseJWT(token, jwtKey string) (*jwt.Token, error) {
return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
method, ok := t.Method.(*jwt.SigningMethodHMAC)
if !ok || method != jwt.SigningMethodHS256 {
return nil, errors.New(publicErrorMessage, "Unexpected signing method")
}
return []byte(viper.GetString("auth.jwt_key")), nil
return []byte(jwtKey), nil
})
}
+3 -3
View File
@@ -13,8 +13,8 @@ import (
)
func TestGenerateJWT(t *testing.T) {
tokenString, err := GenerateJWT("4")
token, err := ParseJWT(tokenString)
tokenString, err := GenerateJWT("4", "")
token, err := ParseJWT(tokenString, "")
if err != nil {
t.Fatal(err.Error())
}
@@ -77,7 +77,7 @@ func TestSessionManager(t *testing.T) {
t.Fatal("No cookie was set")
}
tokenString := strings.Split(header, "=")[1]
token, err := ParseJWT(tokenString)
token, err := ParseJWT(tokenString, "")
if err != nil {
t.Fatal(err.Error())
}
+1 -1
View File
@@ -352,7 +352,7 @@ func TestGetInfoAboutSession(t *testing.T) {
// Get info about sessions for admin1
////////////////////////////////////////////////////////////////////////////
token, err := kolide.ParseJWT(strings.Split(cookie, "=")[1])
token, err := kolide.ParseJWT(strings.Split(cookie, "=")[1], "")
assert.Nil(t, err)
claims, ok := token.Claims.(jwt.MapClaims)
+12 -3
View File
@@ -85,10 +85,19 @@ func NotFoundRequestError(c *gin.Context) {
// }
func NewSessionManager(c *gin.Context) *kolide.SessionManager {
var (
cookieName = viper.GetString("session.cookie_name")
jwtKey = viper.GetString("auth.jwt_key")
)
if cookieName == "" {
cookieName = "KolideSession"
}
return &kolide.SessionManager{
Request: c.Request,
Store: GetDB(c),
Writer: c.Writer,
Request: c.Request,
Store: GetDB(c),
Writer: c.Writer,
CookieName: cookieName,
JWTKey: jwtKey,
}
}