mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
3757aace08
#8129 Apart from fixing the issue in #8129, this change also introduces UUIDs to Fleet errors. To be able to match a returned error from the API to a error in the Fleet logs. See https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for more context. Samples with the changes in this PR: ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d '' { "message": "Bad request", "errors": [ { "name": "base", "reason": "Expected JSON Body" } ], "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0" } ``` ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd' { "message": "Bad request", "errors": [ { "name": "base", "reason": "json decoder error" } ], "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d" } ``` ``` curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams" { "message": "Authentication required", "errors": [ { "name": "base", "reason": "Authentication required" } ], "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Authorization header required", "errors": [ { "name": "base", "reason": "Authorization header required" } ], "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Permission Denied", "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06" } ``` - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
242 lines
6.5 KiB
Go
242 lines
6.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/getsentry/sentry-go"
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
"github.com/go-sql-driver/mysql"
|
|
)
|
|
|
|
// errorer interface is implemented by response structs to encode business logic errors
|
|
type errorer interface {
|
|
error() error
|
|
}
|
|
|
|
type jsonError struct {
|
|
Message string `json:"message"`
|
|
Code int `json:"code,omitempty"`
|
|
Errors []map[string]string `json:"errors,omitempty"`
|
|
UUID string `json:"uuid,omitempty"`
|
|
}
|
|
|
|
// 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 {
|
|
return []map[string]string{
|
|
{
|
|
"name": "base",
|
|
"reason": err,
|
|
},
|
|
}
|
|
}
|
|
|
|
type validationErrorInterface interface {
|
|
error
|
|
Invalid() []map[string]string
|
|
}
|
|
|
|
type permissionErrorInterface interface {
|
|
error
|
|
PermissionError() []map[string]string
|
|
}
|
|
|
|
type badRequestErrorInterface interface {
|
|
error
|
|
BadRequestError() []map[string]string
|
|
}
|
|
|
|
type notFoundErrorInterface interface {
|
|
error
|
|
IsNotFound() bool
|
|
}
|
|
|
|
type existsErrorInterface interface {
|
|
error
|
|
IsExists() bool
|
|
}
|
|
|
|
type conflictErrorInterface interface {
|
|
error
|
|
IsConflict() bool
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// encode error and status header to the client
|
|
func encodeError(ctx context.Context, err error, w http.ResponseWriter) {
|
|
ctxerr.Handle(ctx, err)
|
|
origErr := err
|
|
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
|
|
err = ctxerr.Cause(err)
|
|
|
|
var uuid string
|
|
if uuidErr, ok := err.(fleet.ErrorUUIDer); ok {
|
|
uuid = uuidErr.UUID()
|
|
}
|
|
|
|
jsonErr := jsonError{
|
|
UUID: uuid,
|
|
}
|
|
|
|
switch e := err.(type) {
|
|
case validationErrorInterface:
|
|
if statusErr, ok := e.(statuser); ok {
|
|
w.WriteHeader(statusErr.Status())
|
|
} else {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
}
|
|
jsonErr.Message = "Validation Failed"
|
|
jsonErr.Errors = e.Invalid()
|
|
case permissionErrorInterface:
|
|
jsonErr.Message = "Permission Denied"
|
|
jsonErr.Errors = e.PermissionError()
|
|
w.WriteHeader(http.StatusForbidden)
|
|
case mailError:
|
|
jsonErr.Message = "Mail Error"
|
|
jsonErr.Errors = e.MailError()
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
case *osqueryError:
|
|
// 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(),
|
|
"uuid": uuid,
|
|
}
|
|
if e.NodeInvalid() {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
errMap["node_invalid"] = true
|
|
} else {
|
|
// 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?
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
|
|
enc.Encode(errMap) //nolint:errcheck
|
|
return
|
|
case notFoundErrorInterface:
|
|
jsonErr.Message = "Resource Not Found"
|
|
jsonErr.Errors = baseError(e.Error())
|
|
w.WriteHeader(http.StatusNotFound)
|
|
case existsErrorInterface:
|
|
jsonErr.Message = "Resource Already Exists"
|
|
jsonErr.Errors = baseError(e.Error())
|
|
w.WriteHeader(http.StatusConflict)
|
|
case conflictErrorInterface:
|
|
jsonErr.Message = "Conflict"
|
|
jsonErr.Errors = baseError(e.Error())
|
|
w.WriteHeader(http.StatusConflict)
|
|
case badRequestErrorInterface:
|
|
jsonErr.Message = "Bad request"
|
|
jsonErr.Errors = baseError(e.Error())
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
case *mysql.MySQLError:
|
|
jsonErr.Message = "Validation Failed"
|
|
jsonErr.Errors = baseError(e.Error())
|
|
statusCode := http.StatusUnprocessableEntity
|
|
if e.Number == 1062 {
|
|
statusCode = http.StatusConflict
|
|
}
|
|
w.WriteHeader(statusCode)
|
|
case *fleet.Error:
|
|
jsonErr.Message = e.Error()
|
|
jsonErr.Code = e.Code
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
default:
|
|
// 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) {
|
|
jsonErr.Message = opErr.Error()
|
|
jsonErr.Errors = baseError(opErr.Error())
|
|
w.WriteHeader(http.StatusRequestTimeout)
|
|
enc.Encode(jsonErr) //nolint:errcheck
|
|
return
|
|
}
|
|
if fleet.IsForeignKey(err) {
|
|
jsonErr.Message = "Validation Failed"
|
|
jsonErr.Errors = baseError(err.Error())
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
enc.Encode(jsonErr) //nolint:errcheck
|
|
return
|
|
}
|
|
|
|
// Get specific status code if it is available from this error type,
|
|
// defaulting to HTTP 500
|
|
status := http.StatusInternalServerError
|
|
var sce kithttp.StatusCoder
|
|
if errors.As(err, &sce) {
|
|
status = sce.StatusCode()
|
|
}
|
|
|
|
// See header documentation
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
|
|
var ewra fleet.ErrWithRetryAfter
|
|
if errors.As(err, &ewra) {
|
|
w.Header().Add("Retry-After", strconv.Itoa(ewra.RetryAfter()))
|
|
}
|
|
|
|
msg := err.Error()
|
|
reason := err.Error()
|
|
var ume *fleet.UserMessageError
|
|
if errors.As(err, &ume) {
|
|
if text := http.StatusText(status); text != "" {
|
|
msg = text
|
|
}
|
|
reason = ume.UserMessage()
|
|
}
|
|
|
|
w.WriteHeader(status)
|
|
jsonErr.Message = msg
|
|
jsonErr.Errors = baseError(reason)
|
|
}
|
|
|
|
enc.Encode(jsonErr) //nolint:errcheck
|
|
}
|
|
|
|
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)
|
|
}
|