internal/pkg/api/error.go (563 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package api import ( "context" "encoding/json" "errors" "fmt" "net/http" "os" "strings" "time" "go.elastic.co/apm/v2" "github.com/elastic/fleet-server/v7/internal/pkg/apikey" "github.com/elastic/fleet-server/v7/internal/pkg/dl" "github.com/elastic/fleet-server/v7/internal/pkg/es" "github.com/elastic/fleet-server/v7/internal/pkg/file" "github.com/elastic/fleet-server/v7/internal/pkg/file/delivery" "github.com/elastic/fleet-server/v7/internal/pkg/file/uploader" "github.com/elastic/fleet-server/v7/internal/pkg/limit" "github.com/elastic/fleet-server/v7/internal/pkg/logger" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" ) // Alias logger constants const ( ECSHTTPRequestID = logger.ECSHTTPRequestID ECSEventDuration = logger.ECSEventDuration ECSHTTPResponseCode = logger.ECSHTTPResponseCode ECSHTTPResponseBodyBytes = logger.ECSHTTPResponseBodyBytes LogAPIKeyID = logger.APIKeyID LogPolicyID = logger.PolicyID LogAgentID = logger.AgentID LogEnrollAPIKeyID = logger.EnrollAPIKeyID LogAccessAPIKeyID = logger.AccessAPIKeyID ) // BadRequestErr is used for request validation errors. These can be json // unmarshal errors such as json.SyntaxError, or any other input validation // error. type BadRequestErr struct { msg string nextErr error } func (e *BadRequestErr) Error() string { return fmt.Sprintf("Bad request: %s", e.msg) } func (e *BadRequestErr) Unwrap() error { return e.nextErr } // HTTPErrResp is an HTTP error response type HTTPErrResp struct { StatusCode int `json:"statusCode"` Error string `json:"error"` Message string `json:"message,omitempty"` Level zerolog.Level `json:"-"` } // NewHTTPErrResp creates an ErrResp from a go error func NewHTTPErrResp(err error) HTTPErrResp { errTable := []struct { target error meta HTTPErrResp }{ { ErrAgentNotFound, HTTPErrResp{ http.StatusNotFound, "AgentNotFound", "agent could not be found", zerolog.WarnLevel, }, }, { ErrAPIKeyNotEnabled, HTTPErrResp{ http.StatusUnauthorized, "Unauthorized", "ApiKey not enabled", zerolog.InfoLevel, }, }, { context.Canceled, HTTPErrResp{ 499, "StatusClientClosedRequest", "server is stopping", zerolog.InfoLevel, }, }, { ErrAgentNotReplaceable, HTTPErrResp{ http.StatusForbidden, "AgentNotReplaceable", "existing agent cannot be replaced", zerolog.WarnLevel, }, }, { ErrInvalidUserAgent, HTTPErrResp{ http.StatusBadRequest, "InvalidUserAgent", "user-agent is invalid", zerolog.InfoLevel, }, }, { ErrUnsupportedVersion, HTTPErrResp{ http.StatusBadRequest, "UnsupportedVersion", "version is not supported", zerolog.InfoLevel, }, }, { dl.ErrNotFound, HTTPErrResp{ http.StatusNotFound, "NotFound", "not found", zerolog.WarnLevel, }, }, { ErrorThrottle, HTTPErrResp{ http.StatusTooManyRequests, "TooManyRequests", "too many requests", zerolog.DebugLevel, }, }, { limit.ErrRateLimit, HTTPErrResp{ http.StatusTooManyRequests, "RateLimit", "exceeded the rate limit", zerolog.WarnLevel, }, }, { limit.ErrMaxLimit, HTTPErrResp{ http.StatusTooManyRequests, "MaxLimit", "exceeded the max limit", zerolog.WarnLevel, }, }, { apikey.ErrElasticsearchAuthLimit, HTTPErrResp{ http.StatusTooManyRequests, "ElasticsearchAPIKeyAuthLimit", "exceeded the elasticsearch api key auth limit", zerolog.WarnLevel, }, }, { os.ErrDeadlineExceeded, HTTPErrResp{ http.StatusRequestTimeout, "RequestTimeout", "timeout on request", zerolog.InfoLevel, }, }, { ErrUpdatingInactiveAgent, HTTPErrResp{ http.StatusUnauthorized, "Unauthorized", "Agent not active", zerolog.InfoLevel, }, }, { ErrTransitHashRequired, HTTPErrResp{ http.StatusBadRequest, "TransitHashRequired", "Transit hash required", zerolog.InfoLevel, }, }, { ErrAgentIdentity, HTTPErrResp{ http.StatusForbidden, "ErrAgentIdentity", "Agent header contains wrong identifier", zerolog.InfoLevel, }, }, { ErrAgentCorrupted, HTTPErrResp{ http.StatusBadRequest, "ErrAgentCorrupted", "Agent record corrupted", zerolog.InfoLevel, }, }, { ErrAgentInactive, HTTPErrResp{ http.StatusUnauthorized, "ErrAgentInactive", "Agent inactive", zerolog.InfoLevel, }, }, { ErrAPIKeyNotEnabled, HTTPErrResp{ http.StatusUnauthorized, "ErrAPIKeyNotEnabled", "APIKey not enabled", zerolog.InfoLevel, }, }, { ErrFileInfoBodyRequired, HTTPErrResp{ http.StatusBadRequest, "ErrFileInfoBodyRequired", "file info body is required", zerolog.InfoLevel, }, }, { ErrAgentIDMissing, HTTPErrResp{ http.StatusBadRequest, "ErrAgentIDMissing", "equired field agent_id is missing", zerolog.InfoLevel, }, }, { ErrTLSRequired, HTTPErrResp{ http.StatusNotImplemented, "ErrTLSRequired", "server must run with tls to use this endpoint", zerolog.InfoLevel, }, }, // apikey { apikey.ErrNoAuthHeader, HTTPErrResp{ http.StatusUnauthorized, "ErrNoAuthHeader", "no authorization header", zerolog.InfoLevel, }, }, { apikey.ErrMalformedHeader, HTTPErrResp{ http.StatusBadRequest, "ErrMalformedHeader", "malformed authorization header", zerolog.InfoLevel, }, }, { apikey.ErrUnauthorized, HTTPErrResp{ http.StatusUnauthorized, "ErrUnauthorized", "unauthorized", zerolog.InfoLevel, }, }, { apikey.ErrMalformedToken, HTTPErrResp{ http.StatusBadRequest, "ErrMalformedToken", "malformed token", zerolog.InfoLevel, }, }, { apikey.ErrInvalidToken, HTTPErrResp{ http.StatusUnauthorized, "ErrInvalidToken", "token not valid utf8", zerolog.InfoLevel, }, }, { apikey.ErrAPIKeyNotFound, HTTPErrResp{ http.StatusUnauthorized, "ErrAPIKeyNotFound", "api key not found", zerolog.InfoLevel, }, }, // upload { uploader.ErrInvalidUploadID, HTTPErrResp{ http.StatusBadRequest, "ErrAPIKeyNotFound", "active upload not found with this ID, it may be expired", zerolog.InfoLevel, }, }, { uploader.ErrFileSizeTooLarge, HTTPErrResp{ http.StatusBadRequest, "ErrFileSizeTooLarge", "this file exceeds the maximum allowed file size", zerolog.InfoLevel, }, }, { uploader.ErrMissingChunks, HTTPErrResp{ http.StatusBadRequest, "ErrMissingChunks", "file data incomplete, not all chunks were uploaded", zerolog.InfoLevel, }, }, { uploader.ErrHashMismatch, HTTPErrResp{ http.StatusBadRequest, "ErrHashMismatch", "hash does not match", zerolog.InfoLevel, }, }, { uploader.ErrUploadExpired, HTTPErrResp{ http.StatusBadRequest, "ErrUploadExpired", "upload has expired", zerolog.InfoLevel, }, }, { uploader.ErrUploadStopped, HTTPErrResp{ http.StatusBadRequest, "ErrUploadStopped", "upload has stopped", zerolog.InfoLevel, }, }, { uploader.ErrInvalidChunkNum, HTTPErrResp{ http.StatusBadRequest, "ErrInvalidChunkNum", "invalid chunk number", zerolog.InfoLevel, }, }, { uploader.ErrFailValidation, HTTPErrResp{ http.StatusBadRequest, "ErrFailValidation", "file contents failed validation", zerolog.InfoLevel, }, }, { uploader.ErrStatusNoUploads, HTTPErrResp{ http.StatusBadRequest, "ErrStatusNoUploads", "file closed, not accepting uploads", zerolog.InfoLevel, }, }, { uploader.ErrPayloadRequired, HTTPErrResp{ http.StatusBadRequest, "ErrPayloadRequired", "upload start payload required", zerolog.InfoLevel, }, }, { uploader.ErrFileSizeRequired, HTTPErrResp{ http.StatusBadRequest, "ErrFileSizeRequired", "file.size is required", zerolog.InfoLevel, }, }, { uploader.ErrInvalidFileSize, HTTPErrResp{ http.StatusBadRequest, "ErrInvalidFileSize", "", zerolog.InfoLevel, }, }, { uploader.ErrFieldRequired, HTTPErrResp{ http.StatusBadRequest, "ErrFieldRequired", "", zerolog.InfoLevel, }, }, // Version { ErrInvalidAPIVersionFormat, HTTPErrResp{ http.StatusBadRequest, "ErrInvalidAPIVersionFormat", "", zerolog.InfoLevel, }, }, { ErrUnsupportedAPIVersion, HTTPErrResp{ http.StatusBadRequest, "ErrUnsupportedAPIVersion", "", zerolog.InfoLevel, }, }, // file { delivery.ErrNoFile, HTTPErrResp{ http.StatusNotFound, "ErrNoFile", "file not found", zerolog.InfoLevel, }, }, { file.ErrInvalidID, HTTPErrResp{ http.StatusBadRequest, "ErrInvalidFileID", "ErrInvalidID", zerolog.InfoLevel, }, }, { ErrPolicyNotFound, HTTPErrResp{ http.StatusBadRequest, "ErrPolicyNotFound", "ErrPolicyNotFound", zerolog.InfoLevel, }, }, // audit unenroll { ErrAuditUnenrollReason, HTTPErrResp{ http.StatusConflict, "ErrAuditReasonConflict", "agent document contains audit_unenroll_reason", zerolog.InfoLevel, }, }, } for _, e := range errTable { if errors.Is(err, e.target) { if len(e.meta.Message) == 0 { return HTTPErrResp{ e.meta.StatusCode, e.meta.Error, err.Error(), e.meta.Level, } } return e.meta } } var drErr *BadRequestErr if errors.As(err, &drErr) { return HTTPErrResp{ http.StatusBadRequest, "BadRequest", err.Error(), zerolog.ErrorLevel, } } // If it's a JSON marshal error var jErr *json.MarshalerError if errors.As(err, &jErr) { return HTTPErrResp{ http.StatusInternalServerError, err.Error(), "Fleet server unable to marshall JSON", zerolog.ErrorLevel, } } var esErr *es.ErrElastic if errors.As(err, &esErr) { return HTTPErrResp{ http.StatusServiceUnavailable, esErr.Error(), "elasticsearch error", zerolog.ErrorLevel, } } // Check if we have encountered a connectivity error // Predicate taken from https://github.com/golang/go/blob/go1.17.5/src/net/dial_test.go#L798 if strings.Contains(err.Error(), "connection refused") { return HTTPErrResp{ http.StatusServiceUnavailable, "ServiceUnavailable", "Fleet server unable to communicate with Elasticsearch", zerolog.InfoLevel, } } // Default return HTTPErrResp{ StatusCode: http.StatusInternalServerError, Error: "BadRequest", Message: err.Error(), Level: zerolog.InfoLevel, } } // Write will serialize the ErrResp to an http response and include the proper headers. func (er HTTPErrResp) Write(w http.ResponseWriter) error { data, err := json.Marshal(&er) if err != nil { return err } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(er.StatusCode) _, err = w.Write(data) return err } func ErrorResp(w http.ResponseWriter, r *http.Request, err error) { zlog := hlog.FromRequest(r) resp := NewHTTPErrResp(err) e := zlog.WithLevel(resp.Level).Err(err).Int(ECSHTTPResponseCode, resp.StatusCode).Str("error.type", fmt.Sprintf("%T", err)) if ts, ok := logger.CtxStartTime(r.Context()); ok { e = e.Int64(ECSEventDuration, time.Since(ts).Nanoseconds()) } e.Msg("HTTP request error") if resp.StatusCode >= 500 { if trans := apm.TransactionFromContext(r.Context()); trans != nil { esErr := &es.ErrElastic{} if errors.As(err, &esErr) { trans.Context.SetLabel("error.type", "ErrElastic") trans.Context.SetLabel("error.details.status", esErr.Status) trans.Context.SetLabel("error.details.type", esErr.Type) trans.Context.SetLabel("error.details.reason", esErr.Reason) trans.Context.SetLabel("error.details.cause.type", esErr.Cause.Type) trans.Context.SetLabel("error.details.cause.reason", esErr.Cause.Reason) } else { trans.Context.SetLabel("error.type", fmt.Sprintf("%T", err)) } } apm.CaptureError(r.Context(), err).Send() } if rerr := resp.Write(w); rerr != nil { zlog.Error().Err(rerr).Msg("fail writing error response") } }