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:
@@ -1,4 +1,5 @@
|
||||
# binaries
|
||||
kolide-devserver/kolide-devserver
|
||||
kolide-ose
|
||||
*.exe
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user