Improve error handling throughout backend (#50)

* New function `errors.ReturnError` for writing errors into the HTTP response
* New type `KolideError` that includes additional error context
* Validation and application errors are reported in a consistent JSON format
* Add 404 handler
* Refactored error handling throughout codebase to use new error patterns
This commit is contained in:
Zachary Wasserman 2016-08-09 19:04:28 -07:00 committed by GitHub
parent 2c15647b6e
commit 604e3e4fb0
12 changed files with 787 additions and 429 deletions

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
kolide.exe
kolide
kolide-ose*
vendor/
vendor/
*.test

14
Godeps/Godeps.json generated
View file

@ -20,6 +20,10 @@
"ImportPath": "github.com/alecthomas/units",
"Rev": "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
},
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
},
{
"ImportPath": "github.com/dgrijalva/jwt-go",
"Comment": "v3.0.0-4-g01aeca5",
@ -81,6 +85,16 @@
"Comment": "v1.1.0-71-ge118d44",
"Rev": "e118d4451349065b8e7ce0f0af32e033995363f8"
},
{
"ImportPath": "github.com/pmezard/go-difflib/difflib",
"Comment": "v1.0.0",
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
},
{
"ImportPath": "github.com/stretchr/testify/assert",
"Comment": "v1.1.3-19-gd77da35",
"Rev": "d77da356e56a7428ad25149ca77381849a6a5232"
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "bc89c496413265e715159bdc8478ee9a92fdc265"

View file

@ -3,13 +3,13 @@ package app
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/kolide/kolide-ose/config"
"github.com/kolide/kolide-ose/errors"
"golang.org/x/crypto/bcrypt"
)
@ -38,7 +38,7 @@ func (vc *ViewerContext) UserID() (uint, error) {
if vc.user != nil {
return vc.user.ID, nil
}
return 0, errors.New("No user set")
return 0, errors.New("Unauthorized", "No user set")
}
// CanPerformActions returns a bool indicating the current user's ability to
@ -157,8 +157,8 @@ func SaltAndHashPassword(password string) (string, []byte, error) {
// swagger:parameters Login
type LoginRequestBody struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// swagger:route POST /api/v1/kolide/login Login
@ -185,9 +185,9 @@ type LoginRequestBody struct {
// 200: GetUserResponseBody
func Login(c *gin.Context) {
var body LoginRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing Login post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -212,7 +212,7 @@ func Login(c *gin.Context) {
sm.MakeSessionForUserID(user.ID)
err = sm.Save()
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -252,13 +252,13 @@ func Logout(c *gin.Context) {
err := sm.Destroy()
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
err = sm.Save()
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}

View file

@ -9,24 +9,23 @@ import (
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/kolide/kolide-ose/config"
"github.com/kolide/kolide-ose/osquery"
"github.com/kolide/kolide-ose/sessions"
)
var tables = [...]interface{}{
&User{},
&sessions.Session{},
&osquery.ScheduledQuery{},
&osquery.Pack{},
&osquery.DiscoveryQuery{},
&osquery.Host{},
&osquery.Label{},
&osquery.Option{},
&osquery.Decorator{},
&osquery.Target{},
&osquery.DistributedQuery{},
&osquery.Query{},
&osquery.DistributedQueryExecution{},
&ScheduledQuery{},
&Pack{},
&DiscoveryQuery{},
&Host{},
&Label{},
&Option{},
&Decorator{},
&Target{},
&DistributedQuery{},
&Query{},
&DistributedQueryExecution{},
}
func setDBSettings(db *gorm.DB) {

324
app/osquery.go Normal file
View file

@ -0,0 +1,324 @@
package app
import (
"net/http"
"time"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/kolide/kolide-ose/errors"
)
type ScheduledQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null"`
QueryID int
Query Query
Interval uint `gorm:"not null"`
Snapshot bool
Differential bool
Platform string
PackID uint
}
type Query struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query string `gorm:"not null"`
Targets []Target `gorm:"many2many:query_targets"`
}
type TargetType int
const (
TargetLabel TargetType = iota
TargetHost TargetType = iota
)
type Target struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Type TargetType
QueryID uint
TargetID uint
}
type DistributedQueryStatus int
const (
QueryRunning DistributedQueryStatus = iota
QueryComplete DistributedQueryStatus = iota
QueryError DistributedQueryStatus = iota
)
type DistributedQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query Query
MaxDuration time.Duration
Status DistributedQueryStatus
UserID uint
}
type DistributedQueryExecutionStatus int
const (
ExecutionWaiting DistributedQueryExecutionStatus = iota
ExecutionRequested DistributedQueryExecutionStatus = iota
ExecutionSucceeded DistributedQueryExecutionStatus = iota
ExecutionFailed DistributedQueryExecutionStatus = iota
)
type DistributedQueryExecution struct {
HostID uint
DistributedQueryID uint
Status DistributedQueryExecutionStatus
Error string `gorm:"size:1024"`
ExecutionDuration time.Duration
}
type Pack struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null;unique_index:idx_pack_unique_name"`
Platform string
Queries []ScheduledQuery
DiscoveryQueries []DiscoveryQuery
}
type DiscoveryQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query string `gorm:"size:1024" gorm:"not null"`
}
type Host struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
NodeKey string `gorm:"unique_index:idx_host_unique_nodekey"`
HostName string
UUID string `gorm:"unique_index:idx_host_unique_uuid"`
IPAddress string
Platform string
Labels []*Label `gorm:"many2many:host_labels;"`
}
type Label struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null;unique_index:idx_label_unique_name"`
Query string
Hosts []Host
}
type Option struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Key string `gorm:"not null;unique_index:idx_option_unique_key"`
Value string `gorm:"not null"`
Platform string
}
type DecoratorType int
const (
DecoratorLoad DecoratorType = iota
DecoratorAlways DecoratorType = iota
DecoratorInterval DecoratorType = iota
)
type Decorator struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Type DecoratorType `gorm:"not null"`
Interval int
Query string
}
//
type OsqueryEnrollPostBody struct {
EnrollSecret string `json:"enroll_secret" validate:"required"`
}
type OsqueryConfigPostBody struct {
NodeKey string `json:"node_key" validate:"required"`
}
type OsqueryLogPostBody struct {
NodeKey string `json:"node_key" validate:"required"`
LogType string `json:"log_type" validate:"required"`
Data []map[string]interface{} `json:"data" validate:"required"`
}
type OsqueryResultLog struct {
Name string `json:"name"`
HostIdentifier string `json:"hostIdentifier"`
UnixTime string `json:"unixTime"`
CalendarTime string `json:"calendarTime"`
Columns map[string]string `json:"columns"`
Action string `json:"action"`
}
type OsqueryStatusLog struct {
Severity string `json:"severity"`
Filename string `json:"filename"`
Line string `json:"line"`
Message string `json:"message"`
Version string `json:"version"`
}
type OsqueryDistributedReadPostBody struct {
NodeKey string `json:"node_key" validate:"required"`
}
type OsqueryDistributedWritePostBody struct {
NodeKey string `json:"node_key" validate:"required"`
Queries map[string][]map[string]string `json:"queries" validate:"required"`
}
func OsqueryEnroll(c *gin.Context) {
var body OsqueryEnrollPostBody
err := ParseAndValidateJSON(c, &body)
if err != nil {
errors.ReturnError(c, err)
return
}
logrus.Debugf("OsqueryEnroll: %s", body.EnrollSecret)
c.JSON(http.StatusOK,
gin.H{
"node_key": "7",
"node_invalid": false,
})
}
func OsqueryConfig(c *gin.Context) {
var body OsqueryConfigPostBody
err := ParseAndValidateJSON(c, &body)
if err != nil {
errors.ReturnError(c, err)
return
}
logrus.Debugf("OsqueryConfig: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"schedule": map[string]map[string]interface{}{
"time": {
"query": "select * from time;",
"interval": 1,
},
},
"node_invalid": false,
})
}
func OsqueryLog(c *gin.Context) {
var body OsqueryLogPostBody
err := ParseAndValidateJSON(c, &body)
if err != nil {
errors.ReturnError(c, err)
return
}
logrus.Debugf("OsqueryLog: %s", body.LogType)
if body.LogType == "status" {
for _, data := range body.Data {
var log OsqueryStatusLog
severity, ok := data["severity"].(string)
if ok {
log.Severity = severity
} else {
logrus.Error("Error asserting the type of status log severity")
}
filename, ok := data["filename"].(string)
if ok {
log.Filename = filename
} else {
logrus.Error("Error asserting the type of status log filename")
}
line, ok := data["line"].(string)
if ok {
log.Line = line
} else {
logrus.Error("Error asserting the type of status log line")
}
message, ok := data["message"].(string)
if ok {
log.Message = message
} else {
logrus.Error("Error asserting the type of status log message")
}
version, ok := data["version"].(string)
if ok {
log.Version = version
} else {
logrus.Error("Error asserting the type of status log version")
}
logrus.WithFields(logrus.Fields{
"node_key": body.NodeKey,
"severity": log.Severity,
"filename": log.Filename,
"line": log.Line,
"version": log.Version,
}).Info(log.Message)
}
} else if body.LogType == "result" {
// TODO: handle all of the different kinds of results logs
}
c.JSON(http.StatusOK,
gin.H{
"node_invalid": false,
})
}
func OsqueryDistributedRead(c *gin.Context) {
var body OsqueryDistributedReadPostBody
err := ParseAndValidateJSON(c, &body)
if err != nil {
errors.ReturnError(c, err)
return
}
logrus.Debugf("OsqueryDistributedRead: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"queries": map[string]string{
"id1": "select * from osquery_info",
},
"node_invalid": false,
})
}
func OsqueryDistributedWrite(c *gin.Context) {
var body OsqueryDistributedWritePostBody
err := ParseAndValidateJSON(c, &body)
if err != nil {
errors.ReturnError(c, err)
return
}
logrus.Debugf("OsqueryDistributedWrite: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"node_invalid": false,
})
}

View file

@ -1,7 +1,10 @@
package app
import (
"encoding/json"
"fmt"
"io"
"net/http"
_ "net/http/pprof"
"time"
@ -10,43 +13,32 @@ import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/kolide/kolide-ose/config"
"github.com/kolide/kolide-ose/osquery"
"github.com/kolide/kolide-ose/errors"
"github.com/kolide/kolide-ose/sessions"
"gopkg.in/go-playground/validator.v8"
)
var validate *validator.Validate = validator.New(&validator.Config{TagName: "validate", FieldNameTag: "json"})
// Get the database connection from the context, or panic
func GetDB(c *gin.Context) *gorm.DB {
return c.MustGet("DB").(*gorm.DB)
}
// ServerError is a helper which accepts a string error and returns a map in
// format that is required by gin.Context.JSON
func ServerError(e string) *map[string]interface{} {
return &map[string]interface{}{
"error": e,
}
}
// DatabaseError emits a response that is appropriate in the event that a
// database failure occurs, a record is not found in the database, etc
func DatabaseError(c *gin.Context) {
c.JSON(500, ServerError("Database error"))
}
// UnauthorizedError emits a response that is appropriate in the event that a
// request is received by a user which is not authorized to carry out the
// requested action
func UnauthorizedError(c *gin.Context) {
c.JSON(401, ServerError("Unauthorized"))
}
// MalformedRequestError emits a response that is appropriate in the event that
// a request is received by a user which does not have required fields or is in
// some way malformed
func MalformedRequestError(c *gin.Context) {
c.JSON(400, ServerError("Malformed request"))
errors.ReturnError(
c,
errors.NewWithStatus(
http.StatusUnauthorized,
"Unauthorized",
"Unauthorized",
))
}
// Create a new server for testing purposes with no routes attached
func createEmptyTestServer(db *gorm.DB) *gin.Engine {
server := gin.New()
server.Use(DatabaseMiddleware(db))
@ -73,6 +65,35 @@ func NewSessionManager(c *gin.Context) *sessions.SessionManager {
}
}
// Unmarshal JSON from the gin context into a struct
func parseJSON(c *gin.Context, obj interface{}) error {
decoder := json.NewDecoder(c.Request.Body)
if err := decoder.Decode(obj); err != nil {
return err
}
return nil
}
// Parse JSON into a struct with json.Unmarshal, followed by validation with
// the validator library.
func ParseAndValidateJSON(c *gin.Context, obj interface{}) error {
if err := parseJSON(c, obj); err != nil {
return errors.NewFromError(err, http.StatusBadRequest, "JSON parse error")
}
return validate.Struct(obj)
}
func NotFound(c *gin.Context) {
errors.ReturnError(
c,
errors.NewWithStatus(
http.StatusNotFound,
"Not found",
fmt.Sprintf("Route not found for request: %+v", c.Request),
))
}
// CreateServer creates a gin.Engine HTTP server and configures it to be in a
// state such that it is ready to serve HTTP requests for the kolide application
func CreateServer(db *gorm.DB, w io.Writer) *gin.Engine {
@ -105,6 +126,9 @@ func CreateServer(db *gorm.DB, w io.Writer) *gin.Engine {
recoveryLogger.Out = w
server.Use(gin.RecoveryWithWriter(recoveryLogger.Writer()))
// Set the 404 route
server.NoRoute(NotFound)
v1 := server.Group("/api/v1")
// Kolide application API endpoints
@ -130,11 +154,11 @@ func CreateServer(db *gorm.DB, w io.Writer) *gin.Engine {
// osquery API endpoints
osq := v1.Group("/osquery")
osq.POST("/enroll", osquery.OsqueryEnroll)
osq.POST("/config", osquery.OsqueryConfig)
osq.POST("/log", osquery.OsqueryLog)
osq.POST("/distributed/read", osquery.OsqueryDistributedRead)
osq.POST("/distributed/write", osquery.OsqueryDistributedWrite)
osq.POST("/enroll", OsqueryEnroll)
osq.POST("/config", OsqueryConfig)
osq.POST("/log", OsqueryLog)
osq.POST("/distributed/read", OsqueryDistributedRead)
osq.POST("/distributed/write", OsqueryDistributedWrite)
return server
}

View file

@ -7,6 +7,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/kolide/kolide-ose/errors"
"github.com/kolide/kolide-ose/sessions"
"golang.org/x/crypto/bcrypt"
)
@ -127,9 +128,9 @@ type GetUserResponseBody struct {
// 200: GetUserResponseBody
func GetUser(c *gin.Context) {
var body GetUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing GetUser post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -145,7 +146,7 @@ func GetUser(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -167,9 +168,9 @@ func GetUser(c *gin.Context) {
// swagger:parameters CreateUser
type CreateUserRequestBody struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email" binding:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Email string `json:"email" validate:"required,email"`
Admin bool `json:"admin"`
NeedsPasswordReset bool `json:"needs_password_reset"`
}
@ -196,9 +197,9 @@ type CreateUserRequestBody struct {
// 200: GetUserResponseBody
func CreateUser(c *gin.Context) {
var body CreateUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing CreateUser post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -212,7 +213,7 @@ func CreateUser(c *gin.Context) {
user, err := NewUser(db, body.Username, body.Password, body.Email, body.Admin, body.NeedsPasswordReset)
if err != nil {
logrus.Errorf("Error creating new user: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -259,9 +260,9 @@ type ModifyUserRequestBody struct {
// 200: GetUserResponseBody
func ModifyUser(c *gin.Context) {
var body ModifyUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing ModifyUser post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -278,7 +279,7 @@ func ModifyUser(c *gin.Context) {
db := GetDB(c)
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -296,7 +297,7 @@ func ModifyUser(c *gin.Context) {
err = db.Save(&user).Error
if err != nil {
logrus.Errorf("Error updating user in database: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
c.JSON(200, GetUserResponseBody{
@ -338,9 +339,9 @@ type DeleteUserRequestBody struct {
// 200: nil
func DeleteUser(c *gin.Context) {
var body DeleteUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing DeleteUser post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -356,14 +357,14 @@ func DeleteUser(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
err = db.Delete(&user).Error
if err != nil {
logrus.Errorf("Error deleting user from database: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
c.JSON(200, nil)
@ -374,8 +375,8 @@ type ChangePasswordRequestBody struct {
ID uint `json:"id"`
Username string `json:"username"`
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password" binding:"required"`
NewPasswordConfim string `json:"new_password_confirm" binding:"required"`
NewPassword string `json:"new_password" validate:"required"`
NewPasswordConfim string `json:"new_password_confirm" validate:"required"`
}
// swagger:route PATCH /api/v1/kolide/user/password ChangeUserPassword
@ -402,9 +403,9 @@ type ChangePasswordRequestBody struct {
// 200: GetUserResponseBody
func ChangeUserPassword(c *gin.Context) {
var body ChangePasswordRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing ResetPassword post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -425,7 +426,7 @@ func ChangeUserPassword(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -443,14 +444,14 @@ func ChangeUserPassword(c *gin.Context) {
err = user.SetPassword(db, body.NewPassword)
if err != nil {
logrus.Errorf("Error setting user password: %s", err.Error())
DatabaseError(c) // probably not this
errors.ReturnError(c, errors.DatabaseError(err)) // probably not this
return
}
err = db.Save(&user).Error
if err != nil {
logrus.Errorf("Error updating user in database: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
c.JSON(200, GetUserResponseBody{
@ -493,9 +494,9 @@ type SetUserAdminStateRequestBody struct {
// 200: GetUserResponseBody
func SetUserAdminState(c *gin.Context) {
var body SetUserAdminStateRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing SetUserAdminState post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -511,7 +512,7 @@ func SetUserAdminState(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -519,7 +520,7 @@ func SetUserAdminState(c *gin.Context) {
err = db.Save(&user).Error
if err != nil {
logrus.Errorf("Error updating user in database: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
c.JSON(200, GetUserResponseBody{
@ -562,9 +563,9 @@ type SetUserEnabledStateRequestBody struct {
// 200: GetUserResponseBody
func SetUserEnabledState(c *gin.Context) {
var body SetUserEnabledStateRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf("Error parsing SetUserEnabledState post body: %s", err.Error())
errors.ReturnError(c, err)
return
}
@ -580,7 +581,7 @@ func SetUserEnabledState(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -588,7 +589,7 @@ func SetUserEnabledState(c *gin.Context) {
err = db.Save(&user).Error
if err != nil {
logrus.Errorf("Error updating user in database: %s", err.Error())
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
c.JSON(200, GetUserResponseBody{
@ -624,7 +625,7 @@ func GetSessionBackend(c *gin.Context) sessions.SessionBackend {
// swagger:parameters DeleteSession
type DeleteSessionRequestBody struct {
SessionID uint `json:"session_id" binding:"required"`
SessionID uint `json:"session_id" validate:"required"`
}
// swagger:route DELETE /api/v1/kolide/session DeleteSession
@ -649,9 +650,9 @@ type DeleteSessionRequestBody struct {
// 200: nil
func DeleteSession(c *gin.Context) {
var body DeleteSessionRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf(err.Error())
errors.ReturnError(c, err)
return
}
@ -672,7 +673,7 @@ func DeleteSession(c *gin.Context) {
user := &User{ID: session.UserID}
err = db.Where(user).First(user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -683,7 +684,7 @@ func DeleteSession(c *gin.Context) {
err = sb.Destroy(session)
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -719,9 +720,10 @@ type DeleteSessionsForUserRequestBody struct {
// 200: nil
func DeleteSessionsForUser(c *gin.Context) {
var body DeleteSessionsForUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf(err.Error())
errors.ReturnError(c, err)
return
}
vc := VC(c)
@ -736,7 +738,7 @@ func DeleteSessionsForUser(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -748,7 +750,7 @@ func DeleteSessionsForUser(c *gin.Context) {
sb := GetSessionBackend(c)
err = sb.DestroyAllForUser(user.ID)
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -758,7 +760,7 @@ func DeleteSessionsForUser(c *gin.Context) {
// swagger:parameters GetInfoAboutSession
type GetInfoAboutSessionRequestBody struct {
SessionKey string `json:"session_key" binding:"required"`
SessionKey string `json:"session_key" validate:"required"`
}
// swagger:response SessionInfoResponseBody
@ -791,9 +793,9 @@ type SessionInfoResponseBody struct {
// 200: SessionInfoResponseBody
func GetInfoAboutSession(c *gin.Context) {
var body GetInfoAboutSessionRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf(err.Error())
errors.ReturnError(c, err)
return
}
@ -806,7 +808,7 @@ func GetInfoAboutSession(c *gin.Context) {
sb := GetSessionBackend(c)
session, err := sb.FindKey(body.SessionKey)
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -815,7 +817,7 @@ func GetInfoAboutSession(c *gin.Context) {
user.ID = session.UserID
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -865,9 +867,9 @@ type GetInfoAboutSessionsForUserResponseBody struct {
// 200: GetInfoAboutSessionsForUserResponseBody
func GetInfoAboutSessionsForUser(c *gin.Context) {
var body GetInfoAboutSessionsForUserRequestBody
err := c.BindJSON(&body)
err := ParseAndValidateJSON(c, &body)
if err != nil {
logrus.Errorf(err.Error())
errors.ReturnError(c, err)
return
}
@ -883,7 +885,7 @@ func GetInfoAboutSessionsForUser(c *gin.Context) {
user.Username = body.Username
err = db.Where(&user).First(&user).Error
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}
@ -895,7 +897,7 @@ func GetInfoAboutSessionsForUser(c *gin.Context) {
sb := GetSessionBackend(c)
sessions, err := sb.FindAllForUser(user.ID)
if err != nil {
DatabaseError(c)
errors.ReturnError(c, errors.DatabaseError(err))
return
}

104
errors/errors.go Normal file
View file

@ -0,0 +1,104 @@
package errors
import (
"net/http"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"gopkg.in/go-playground/validator.v8"
)
// Kolide's internal representation for errors. It can be used to wrap another
// error (stored in Err), and additionally contains fields for public
// (PublicMessage) and private (PrivateMessage) error messages as well as the
// HTTP status code (StatusCode) corresponding to the error.
type KolideError struct {
Err error
StatusCode int
PublicMessage string
PrivateMessage string
}
// Implementation of error interface
func (e *KolideError) Error() string {
return e.PublicMessage
}
// Create a new KolideError specifying the public and private messages. The
// status code will be set to 500.
func New(publicMessage, privateMessage string) *KolideError {
return &KolideError{
StatusCode: http.StatusInternalServerError,
PublicMessage: publicMessage,
PrivateMessage: privateMessage,
}
}
// Create a new KolideError specifying the HTTP status, and public and private
// messages.
func NewWithStatus(status int, publicMessage, privateMessage string) *KolideError {
return &KolideError{
StatusCode: status,
PublicMessage: publicMessage,
PrivateMessage: privateMessage,
}
}
// Create a new KolideError from an error type. The public message and status
// code should be specified, while the private message will be drawn from
// err.Error()
func NewFromError(err error, status int, publicMessage string) *KolideError {
return &KolideError{
Err: err,
StatusCode: status,
PublicMessage: publicMessage,
PrivateMessage: err.Error(),
}
}
// Wrap a DB error wit the extra KolideError decorations
func DatabaseError(err error) *KolideError {
return NewFromError(err, http.StatusInternalServerError, "Database error")
}
// The status code returned for validation errors. Inspired by the Github API.
const StatusUnprocessableEntity = 422
// Handle an error, printing debug information, writing to the HTTP response as
// appropriate for the dynamic error type.
func ReturnError(c *gin.Context, err error) {
switch typedErr := err.(type) {
case *KolideError:
c.JSON(typedErr.StatusCode,
gin.H{"message": typedErr.PublicMessage})
logrus.WithError(typedErr.Err).Debug(typedErr.PrivateMessage)
case validator.ValidationErrors:
errors := make([](map[string]string), 0, len(typedErr))
for _, fieldErr := range typedErr {
m := make(map[string]string)
m["field"] = fieldErr.Name
m["code"] = "invalid"
m["message"] = fieldErr.Tag
errors = append(errors, m)
}
c.JSON(StatusUnprocessableEntity,
gin.H{"message": "Validation error",
"errors": errors,
})
logrus.WithError(typedErr).Debug("Validation error")
case gorm.Errors, *gorm.Errors:
c.JSON(http.StatusInternalServerError,
gin.H{"message": "Database error"})
logrus.WithError(typedErr).Debug(typedErr.Error())
default:
c.JSON(http.StatusInternalServerError,
gin.H{"message": "Unspecified error"})
logrus.WithError(typedErr).Debug("Unspecified error")
}
}

211
errors/errors_test.go Normal file
View file

@ -0,0 +1,211 @@
package errors
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"sort"
"testing"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
)
func TestNew(t *testing.T) {
kolideErr := New("Public message", "Private message")
expect := &KolideError{
Err: nil,
StatusCode: http.StatusInternalServerError,
PublicMessage: "Public message",
PrivateMessage: "Private message",
}
assert.Equal(t, expect, kolideErr)
}
func TestNewWithStatus(t *testing.T) {
kolideErr := NewWithStatus(http.StatusUnauthorized, "Public message", "Private message")
expect := &KolideError{
Err: nil,
StatusCode: http.StatusUnauthorized,
PublicMessage: "Public message",
PrivateMessage: "Private message",
}
assert.Equal(t, expect, kolideErr)
}
func TestNewFromError(t *testing.T) {
err := errors.New("Foo error")
kolideErr := NewFromError(err, StatusUnprocessableEntity, "Public error")
assert.Equal(t, "Public error", kolideErr.Error())
expect := &KolideError{
Err: err,
StatusCode: StatusUnprocessableEntity,
PublicMessage: "Public error",
PrivateMessage: "Foo error",
}
assert.Equal(t, expect, kolideErr)
}
func TestDatabaseError(t *testing.T) {
err := errors.New("Foo error")
kolideErr := DatabaseError(err)
expect := &KolideError{
Err: err,
StatusCode: http.StatusInternalServerError,
PublicMessage: "Database error",
PrivateMessage: "Foo error",
}
assert.Equal(t, expect, kolideErr)
}
func TestReturnErrorUnspecified(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, errors.New("foo"))
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusInternalServerError {
t.Errorf("Should respond with 500, got %d", resp.Code)
}
expect := `{"message": "Unspecified error"}`
assert.JSONEq(t, expect, resp.Body.String())
}
func TestReturnErrorKolideError(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, &KolideError{
StatusCode: http.StatusUnauthorized,
PublicMessage: "Some error",
})
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Errorf("Should respond with 403, got %d", resp.Code)
}
expect := `{"message": "Some error"}`
assert.JSONEq(t, expect, resp.Body.String())
}
// These types and functions for performing an unordered comparison on a
// []map[string]string] as parsed from the error JSON
type errorField map[string]string
type errorFields []errorField
func (e errorFields) Len() int {
return len(e)
}
func (e errorFields) Less(i, j int) bool {
return e[i]["field"] <= e[j]["field"] &&
e[i]["code"] <= e[j]["code"] &&
e[i]["message"] <= e[j]["message"]
}
func (e errorFields) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func TestReturnErrorValidationError(t *testing.T) {
r := gin.New()
type Foo struct {
Email string `json:"email_foo" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
validate := validator.New(&validator.Config{TagName: "validate", FieldNameTag: "json"})
r.POST("/foo", func(c *gin.Context) {
ReturnError(c, validate.Struct(&Foo{Email: "foo", Password: ""}))
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != StatusUnprocessableEntity {
t.Errorf("Should respond with 422, got %d", resp.Code)
}
t.Log(resp.Body.String())
var bodyJson map[string]interface{}
if err := json.Unmarshal(resp.Body.Bytes(), &bodyJson); err != nil {
t.Errorf("Error unmarshaling JSON: %s", err.Error())
}
assert.Equal(t, "Validation error", bodyJson["message"])
fields, ok := bodyJson["errors"].([]interface{})
if !ok {
t.Errorf("Unexpected type for errors")
}
// The error fields must be copied from []interface{} to
// []map[string][string] before we can sort
compFields := make(errorFields, 0, 0)
for _, field := range fields {
field := field.(map[string]interface{})
compFields = append(
compFields,
errorField{
"code": field["code"].(string),
"field": field["field"].(string),
"message": field["message"].(string),
})
}
expect := errorFields{
{"code": "invalid", "field": "email_foo", "message": "email"},
{"code": "invalid", "field": "password", "message": "required"},
}
// Sort to standardize ordering before comparison
sort.Sort(compFields)
sort.Sort(expect)
assert.Equal(t, expect, compFields)
}
func TestReturnErrorGormError(t *testing.T) {
r := gin.New()
r.POST("/foo", func(c *gin.Context) {
err := gorm.Errors{}
err.Add(gorm.ErrInvalidSQL)
ReturnError(c, err)
})
req, _ := http.NewRequest("POST", "/foo", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusInternalServerError {
t.Errorf("Should respond with 500, got %d", resp.Code)
}
assert.JSONEq(t, `{"message": "Database error"}`, resp.Body.String())
}

View file

@ -1,140 +0,0 @@
package osquery
import "time"
type ScheduledQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null"`
QueryID int
Query Query
Interval uint `gorm:"not null"`
Snapshot bool
Differential bool
Platform string
PackID uint
}
type Query struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query string `gorm:"not null"`
Targets []Target `gorm:"many2many:query_targets"`
}
type TargetType int
const (
TargetLabel TargetType = iota
TargetHost TargetType = iota
)
type Target struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Type TargetType
QueryID uint
TargetID uint
}
type DistributedQueryStatus int
const (
QueryRunning DistributedQueryStatus = iota
QueryComplete DistributedQueryStatus = iota
QueryError DistributedQueryStatus = iota
)
type DistributedQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query Query
MaxDuration time.Duration
Status DistributedQueryStatus
UserID uint
}
type DistributedQueryExecutionStatus int
const (
ExecutionWaiting DistributedQueryExecutionStatus = iota
ExecutionRequested DistributedQueryExecutionStatus = iota
ExecutionSucceeded DistributedQueryExecutionStatus = iota
ExecutionFailed DistributedQueryExecutionStatus = iota
)
type DistributedQueryExecution struct {
HostID uint
DistributedQueryID uint
Status DistributedQueryExecutionStatus
Error string `gorm:"size:1024"`
ExecutionDuration time.Duration
}
type Pack struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null;unique_index:idx_pack_unique_name"`
Platform string
Queries []ScheduledQuery
DiscoveryQueries []DiscoveryQuery
}
type DiscoveryQuery struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Query string `gorm:"size:1024" gorm:"not null"`
}
type Host struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
NodeKey string `gorm:"unique_index:idx_host_unique_nodekey"`
HostName string
UUID string `gorm:"unique_index:idx_host_unique_uuid"`
IPAddress string
Platform string
Labels []*Label `gorm:"many2many:host_labels;"`
}
type Label struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"not null;unique_index:idx_label_unique_name"`
Query string
Hosts []Host
}
type Option struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Key string `gorm:"not null;unique_index:idx_option_unique_key"`
Value string `gorm:"not null"`
Platform string
}
type DecoratorType int
const (
DecoratorLoad DecoratorType = iota
DecoratorAlways DecoratorType = iota
DecoratorInterval DecoratorType = iota
)
type Decorator struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Type DecoratorType `gorm:"not null"`
Interval int
Query string
}

View file

@ -1,183 +0,0 @@
package osquery
import (
"net/http"
"github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
)
type OsqueryEnrollPostBody struct {
EnrollSecret string `json:"enroll_secret" binding:"required"`
}
type OsqueryConfigPostBody struct {
NodeKey string `json:"node_key" binding:"required"`
}
type OsqueryLogPostBody struct {
NodeKey string `json:"node_key" binding:"required"`
LogType string `json:"log_type" binding:"required"`
Data []map[string]interface{} `json:"data" binding:"required"`
}
type OsqueryResultLog struct {
Name string `json:"name"`
HostIdentifier string `json:"hostIdentifier"`
UnixTime string `json:"unixTime"`
CalendarTime string `json:"calendarTime"`
Columns map[string]string `json:"columns"`
Action string `json:"action"`
}
type OsqueryStatusLog struct {
Severity string `json:"severity"`
Filename string `json:"filename"`
Line string `json:"line"`
Message string `json:"message"`
Version string `json:"version"`
}
type OsqueryDistributedReadPostBody struct {
NodeKey string `json:"node_key" binding:"required"`
}
type OsqueryDistributedWritePostBody struct {
NodeKey string `json:"node_key" binding:"required"`
Queries map[string][]map[string]string `json:"queries" binding:"required"`
}
func OsqueryEnroll(c *gin.Context) {
var body OsqueryEnrollPostBody
err := c.BindJSON(&body)
if err != nil {
logrus.Debugf("Error parsing OsqueryEnroll POST body: %s", err.Error())
return
}
logrus.Debugf("OsqueryEnroll: %s", body.EnrollSecret)
c.JSON(http.StatusOK,
gin.H{
"node_key": "7",
"node_invalid": false,
})
}
func OsqueryConfig(c *gin.Context) {
var body OsqueryConfigPostBody
err := c.BindJSON(&body)
if err != nil {
logrus.Debugf("Error parsing OsqueryConfig POST body: %s", err.Error())
return
}
logrus.Debugf("OsqueryConfig: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"schedule": map[string]map[string]interface{}{
"time": {
"query": "select * from time;",
"interval": 1,
},
},
"node_invalid": false,
})
}
func OsqueryLog(c *gin.Context) {
var body OsqueryLogPostBody
err := c.BindJSON(&body)
if err != nil {
logrus.Debugf("Error parsing OsqueryLog POST body: %s", err.Error())
return
}
logrus.Debugf("OsqueryLog: %s", body.LogType)
if body.LogType == "status" {
for _, data := range body.Data {
var log OsqueryStatusLog
severity, ok := data["severity"].(string)
if ok {
log.Severity = severity
} else {
logrus.Error("Error asserting the type of status log severity")
}
filename, ok := data["filename"].(string)
if ok {
log.Filename = filename
} else {
logrus.Error("Error asserting the type of status log filename")
}
line, ok := data["line"].(string)
if ok {
log.Line = line
} else {
logrus.Error("Error asserting the type of status log line")
}
message, ok := data["message"].(string)
if ok {
log.Message = message
} else {
logrus.Error("Error asserting the type of status log message")
}
version, ok := data["version"].(string)
if ok {
log.Version = version
} else {
logrus.Error("Error asserting the type of status log version")
}
logrus.WithFields(logrus.Fields{
"node_key": body.NodeKey,
"severity": log.Severity,
"filename": log.Filename,
"line": log.Line,
"version": log.Version,
}).Info(log.Message)
}
} else if body.LogType == "result" {
// TODO: handle all of the different kinds of results logs
}
c.JSON(http.StatusOK,
gin.H{
"node_invalid": false,
})
}
func OsqueryDistributedRead(c *gin.Context) {
var body OsqueryDistributedReadPostBody
err := c.BindJSON(&body)
if err != nil {
logrus.Debugf("Error parsing OsqueryDistributedRead POST body: %s", err.Error())
return
}
logrus.Debugf("OsqueryDistributedRead: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"queries": map[string]string{
"id1": "select * from osquery_info",
},
"node_invalid": false,
})
}
func OsqueryDistributedWrite(c *gin.Context) {
var body OsqueryDistributedWritePostBody
err := c.BindJSON(&body)
if err != nil {
logrus.Debugf("Error parsing OsqueryDistributedWrite POST body: %s", err.Error())
return
}
logrus.Debugf("OsqueryDistributedWrite: %s", body.NodeKey)
c.JSON(http.StatusOK,
gin.H{
"node_invalid": false,
})
}

View file

@ -1,30 +1,32 @@
package sessions
import (
"errors"
"net/http"
"time"
"github.com/Sirupsen/logrus"
"github.com/dgrijalva/jwt-go"
"github.com/kolide/kolide-ose/errors"
)
const publicErrorMessage string = "Session error"
var (
// An error returned by SessionBackend.Get() if no session record was found
// in the database
ErrNoActiveSession = errors.New("Active session is not present in the database")
ErrNoActiveSession = errors.New(publicErrorMessage, "Active session is not present in the database")
// An error returned by SessionBackend methods when no session object has
// been created yet but the requested action requires one
ErrSessionNotCreated = errors.New("The session has not been created")
ErrSessionNotCreated = errors.New(publicErrorMessage, "The session has not been created")
// An error returned by SessionBackend.Get() when a session is requested but
// it has expired
ErrSessionExpired = errors.New("The session has expired")
ErrSessionExpired = errors.New(publicErrorMessage, "The session has expired")
// An error returned by SessionBackend which indicates that the token
// or it's content were malformed
ErrSessionMalformed = errors.New("The session token was malformed")
ErrSessionMalformed = errors.New(publicErrorMessage, "The session token was malformed")
)
var (
@ -205,7 +207,7 @@ func ParseJWT(token 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("Unexpected signing method")
return nil, errors.New(publicErrorMessage, "Unexpected signing method")
}
return []byte(jwtKey), nil
})