2016-09-26 18:48:55 +00:00
|
|
|
package service
|
2016-09-23 02:41:58 +00:00
|
|
|
|
|
|
|
import (
|
2017-03-15 15:55:30 +00:00
|
|
|
"context"
|
2016-09-23 02:41:58 +00:00
|
|
|
"encoding/json"
|
2021-11-22 14:13:26 +00:00
|
|
|
"errors"
|
2022-01-20 19:41:02 +00:00
|
|
|
"fmt"
|
2022-10-11 16:58:52 +00:00
|
|
|
"net"
|
2021-08-24 17:35:03 +00:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
|
2021-11-15 14:11:38 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2022-01-20 19:41:02 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
2021-06-26 04:46:51 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2022-01-20 19:41:02 +00:00
|
|
|
"github.com/getsentry/sentry-go"
|
2021-06-16 17:55:41 +00:00
|
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
2021-07-08 15:50:43 +00:00
|
|
|
"github.com/go-sql-driver/mysql"
|
2016-09-23 02:41:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// erroer interface is implemented by response structs to encode business logic errors
|
|
|
|
type errorer interface {
|
|
|
|
error() error
|
|
|
|
}
|
|
|
|
|
|
|
|
type jsonError struct {
|
|
|
|
Message string `json:"message"`
|
2021-07-13 19:33:04 +00:00
|
|
|
Code int `json:"code,omitempty"`
|
2016-09-23 02:41:58 +00:00
|
|
|
Errors []map[string]string `json:"errors,omitempty"`
|
2016-12-30 00:40:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// use baseError to encode an jsonError.Errors field with an error that has
|
|
|
|
// a generic "name" field. The frontend client always expects errors in a
|
|
|
|
// []map[string]string format.
|
|
|
|
func baseError(err string) []map[string]string {
|
2021-06-16 17:55:41 +00:00
|
|
|
return []map[string]string{
|
|
|
|
{
|
|
|
|
"name": "base",
|
|
|
|
"reason": err,
|
|
|
|
},
|
2016-12-30 00:40:12 +00:00
|
|
|
}
|
2016-09-23 02:41:58 +00:00
|
|
|
}
|
|
|
|
|
2021-07-08 15:50:43 +00:00
|
|
|
type validationErrorInterface interface {
|
|
|
|
error
|
|
|
|
Invalid() []map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
type permissionErrorInterface interface {
|
|
|
|
error
|
|
|
|
PermissionError() []map[string]string
|
|
|
|
}
|
|
|
|
|
2021-09-29 16:13:23 +00:00
|
|
|
type badRequestErrorInterface interface {
|
|
|
|
error
|
|
|
|
BadRequestError() []map[string]string
|
|
|
|
}
|
|
|
|
|
2021-07-08 15:50:43 +00:00
|
|
|
type notFoundErrorInterface interface {
|
|
|
|
error
|
|
|
|
IsNotFound() bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type existsErrorInterface interface {
|
|
|
|
error
|
|
|
|
IsExists() bool
|
|
|
|
}
|
|
|
|
|
2022-01-20 19:41:02 +00:00
|
|
|
func encodeErrorAndTrySentry(sentryEnabled bool) func(ctx context.Context, err error, w http.ResponseWriter) {
|
|
|
|
if !sentryEnabled {
|
|
|
|
return encodeError
|
|
|
|
}
|
|
|
|
return func(ctx context.Context, err error, w http.ResponseWriter) {
|
|
|
|
encodeError(ctx, err, w)
|
|
|
|
sendToSentry(ctx, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-23 02:41:58 +00:00
|
|
|
// encode error and status header to the client
|
|
|
|
func encodeError(ctx context.Context, err error, w http.ResponseWriter) {
|
2021-11-15 14:11:38 +00:00
|
|
|
ctxerr.Handle(ctx, err)
|
2022-10-11 16:58:52 +00:00
|
|
|
origErr := err
|
2021-11-15 14:11:38 +00:00
|
|
|
|
2016-09-23 02:41:58 +00:00
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
|
2021-11-15 14:11:38 +00:00
|
|
|
err = ctxerr.Cause(err)
|
2021-07-08 15:50:43 +00:00
|
|
|
|
|
|
|
switch e := err.(type) {
|
|
|
|
case validationErrorInterface:
|
2016-09-23 02:41:58 +00:00
|
|
|
ve := jsonError{
|
|
|
|
Message: "Validation Failed",
|
|
|
|
Errors: e.Invalid(),
|
|
|
|
}
|
2022-03-15 19:14:42 +00:00
|
|
|
if statusErr, ok := e.(statuser); ok {
|
|
|
|
w.WriteHeader(statusErr.Status())
|
|
|
|
} else {
|
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
|
|
}
|
2016-09-23 02:41:58 +00:00
|
|
|
enc.Encode(ve)
|
2021-07-08 15:50:43 +00:00
|
|
|
case permissionErrorInterface:
|
2016-09-23 02:41:58 +00:00
|
|
|
pe := jsonError{
|
|
|
|
Message: "Permission Denied",
|
|
|
|
Errors: e.PermissionError(),
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
enc.Encode(pe)
|
|
|
|
return
|
2021-07-08 15:50:43 +00:00
|
|
|
case mailError:
|
2017-01-11 02:00:46 +00:00
|
|
|
me := jsonError{
|
|
|
|
Message: "Mail Error",
|
|
|
|
Errors: e.MailError(),
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
enc.Encode(me)
|
2021-07-08 15:50:43 +00:00
|
|
|
case osqueryError:
|
2016-09-29 04:21:39 +00:00
|
|
|
// osquery expects to receive the node_invalid key when a TLS
|
|
|
|
// request provides an invalid node_key for authentication. It
|
|
|
|
// doesn't use the error message provided, but we provide this
|
|
|
|
// for debugging purposes (and perhaps osquery will use this
|
|
|
|
// error message in the future).
|
|
|
|
|
|
|
|
errMap := map[string]interface{}{"error": e.Error()}
|
|
|
|
if e.NodeInvalid() {
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
errMap["node_invalid"] = true
|
|
|
|
} else {
|
2022-03-07 18:10:55 +00:00
|
|
|
// TODO: osqueryError is not always the result of an internal error on
|
|
|
|
// our side, it is also used to represent a client error (invalid data,
|
|
|
|
// e.g. malformed json, carve too large, etc., so 4xx), are we returning
|
|
|
|
// a 500 because of some osquery-specific requirement?
|
2016-09-29 04:21:39 +00:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
|
|
|
|
|
|
|
enc.Encode(errMap)
|
2021-07-08 15:50:43 +00:00
|
|
|
case notFoundErrorInterface:
|
2016-12-20 18:35:22 +00:00
|
|
|
je := jsonError{
|
|
|
|
Message: "Resource Not Found",
|
2016-12-30 00:40:12 +00:00
|
|
|
Errors: baseError(e.Error()),
|
2016-12-20 18:35:22 +00:00
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
enc.Encode(je)
|
2021-07-08 15:50:43 +00:00
|
|
|
case existsErrorInterface:
|
2016-12-20 18:35:22 +00:00
|
|
|
je := jsonError{
|
|
|
|
Message: "Resource Already Exists",
|
2016-12-30 00:40:12 +00:00
|
|
|
Errors: baseError(e.Error()),
|
2016-12-20 18:35:22 +00:00
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusConflict)
|
|
|
|
enc.Encode(je)
|
2021-09-29 16:13:23 +00:00
|
|
|
case badRequestErrorInterface:
|
|
|
|
je := jsonError{
|
|
|
|
Message: "Bad request",
|
|
|
|
Errors: baseError(e.Error()),
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
enc.Encode(je)
|
2021-07-08 15:50:43 +00:00
|
|
|
case *mysql.MySQLError:
|
|
|
|
je := jsonError{
|
|
|
|
Message: "Validation Failed",
|
|
|
|
Errors: baseError(e.Error()),
|
|
|
|
}
|
|
|
|
statusCode := http.StatusUnprocessableEntity
|
|
|
|
if e.Number == 1062 {
|
|
|
|
statusCode = http.StatusConflict
|
|
|
|
}
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
enc.Encode(je)
|
2021-07-13 19:33:04 +00:00
|
|
|
case *fleet.Error:
|
|
|
|
je := jsonError{
|
|
|
|
Message: e.Error(),
|
|
|
|
Code: e.Code,
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
|
|
enc.Encode(je)
|
2021-07-08 15:50:43 +00:00
|
|
|
default:
|
2022-10-11 16:58:52 +00:00
|
|
|
// when there's a tcp read timeout, the error is *net.OpError but the cause is an internal
|
|
|
|
// poll.DeadlineExceeded which we cannot match against, so we match against the original error
|
|
|
|
var opErr *net.OpError
|
|
|
|
if errors.As(origErr, &opErr) {
|
|
|
|
w.WriteHeader(http.StatusRequestTimeout)
|
|
|
|
je := jsonError{
|
|
|
|
Message: opErr.Error(),
|
|
|
|
Errors: baseError(opErr.Error()),
|
|
|
|
}
|
|
|
|
enc.Encode(je)
|
|
|
|
return
|
|
|
|
}
|
2021-11-15 14:11:38 +00:00
|
|
|
if fleet.IsForeignKey(ctxerr.Cause(err)) {
|
2021-07-08 15:50:43 +00:00
|
|
|
ve := jsonError{
|
|
|
|
Message: "Validation Failed",
|
|
|
|
Errors: baseError(err.Error()),
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
|
|
enc.Encode(ve)
|
|
|
|
return
|
|
|
|
}
|
2016-12-20 18:35:22 +00:00
|
|
|
|
2021-07-08 15:50:43 +00:00
|
|
|
// Get specific status code if it is available from this error type,
|
|
|
|
// defaulting to HTTP 500
|
|
|
|
status := http.StatusInternalServerError
|
2021-11-15 14:11:38 +00:00
|
|
|
var sce kithttp.StatusCoder
|
|
|
|
if errors.As(err, &sce) {
|
|
|
|
status = sce.StatusCode()
|
2021-07-08 15:50:43 +00:00
|
|
|
}
|
2021-03-25 02:36:30 +00:00
|
|
|
|
2021-07-08 15:50:43 +00:00
|
|
|
// See header documentation
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
|
2021-11-15 14:11:38 +00:00
|
|
|
var ewra fleet.ErrWithRetryAfter
|
|
|
|
if errors.As(err, &ewra) {
|
|
|
|
w.Header().Add("Retry-After", strconv.Itoa(ewra.RetryAfter()))
|
2021-07-08 15:50:43 +00:00
|
|
|
}
|
2021-03-26 18:23:29 +00:00
|
|
|
|
2021-07-08 15:50:43 +00:00
|
|
|
w.WriteHeader(status)
|
|
|
|
je := jsonError{
|
|
|
|
Message: err.Error(),
|
|
|
|
Errors: baseError(err.Error()),
|
|
|
|
}
|
|
|
|
enc.Encode(je)
|
2016-12-30 00:40:12 +00:00
|
|
|
}
|
2016-09-23 02:41:58 +00:00
|
|
|
}
|
2022-01-20 19:41:02 +00:00
|
|
|
|
|
|
|
func sendToSentry(ctx context.Context, err error) {
|
|
|
|
v, haveUser := viewer.FromContext(ctx)
|
|
|
|
h, haveHost := host.FromContext(ctx)
|
|
|
|
localHub := sentry.CurrentHub().Clone()
|
|
|
|
if haveUser {
|
|
|
|
localHub.ConfigureScope(func(scope *sentry.Scope) {
|
|
|
|
scope.SetTag("email", v.User.Email)
|
|
|
|
scope.SetTag("user_id", fmt.Sprint(v.User.ID))
|
|
|
|
})
|
|
|
|
} else if haveHost {
|
|
|
|
localHub.ConfigureScope(func(scope *sentry.Scope) {
|
|
|
|
scope.SetTag("hostname", h.Hostname)
|
|
|
|
scope.SetTag("host_id", fmt.Sprint(h.ID))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
localHub.CaptureException(err)
|
|
|
|
}
|