2018-05-04 16:53:21 +00:00
package service
2018-05-09 23:54:23 +00:00
import (
2025-09-19 22:00:19 +00:00
"bytes"
2022-02-28 12:34:44 +00:00
"database/sql"
2018-05-09 23:54:23 +00:00
"encoding/json"
2022-02-14 16:43:34 +00:00
"errors"
2023-04-03 18:25:49 +00:00
"fmt"
2018-05-09 23:54:23 +00:00
"io"
2025-09-19 22:00:19 +00:00
"strings"
Add UUID to Fleet errors and clean up error msgs (#10411)
#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)).~
2023-03-13 16:44:06 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2018-05-09 23:54:23 +00:00
)
2022-02-14 16:43:34 +00:00
var (
2025-03-17 16:44:59 +00:00
ErrUnauthenticated = errors . New ( "unauthenticated, or invalid token" )
ErrPasswordResetRequired = errors . New ( "Password reset required. Please sign into the Fleet UI to update your password, then log in again with: fleetctl login." )
ErrMissingLicense = errors . New ( "missing or invalid license" )
2022-02-14 16:43:34 +00:00
)
2018-05-04 16:53:21 +00:00
type SetupAlreadyErr interface {
SetupAlready ( ) bool
Error ( ) string
}
2018-05-08 02:07:00 +00:00
type setupAlreadyErr struct { }
2018-05-04 16:53:21 +00:00
func ( e setupAlreadyErr ) Error ( ) string {
2021-01-28 15:57:32 +00:00
return "Fleet has already been setup"
2018-05-04 16:53:21 +00:00
}
func ( e setupAlreadyErr ) SetupAlready ( ) bool {
return true
}
type NotSetupErr interface {
NotSetup ( ) bool
Error ( ) string
}
2018-05-08 02:07:00 +00:00
type notSetupErr struct { }
2018-05-04 16:53:21 +00:00
func ( e notSetupErr ) Error ( ) string {
2021-01-28 15:57:32 +00:00
return "The Fleet instance is not set up yet"
2018-05-04 16:53:21 +00:00
}
func ( e notSetupErr ) NotSetup ( ) bool {
return true
}
2018-05-08 02:07:00 +00:00
2023-04-03 18:25:49 +00:00
// TODO: we have a similar but different interface in the fleet package,
// fleet.NotFoundError - at the very least, the NotFound method should be the
// same in both (the other is currently IsNotFound), and ideally we'd just have
// one of those interfaces.
2018-05-08 02:07:00 +00:00
type NotFoundErr interface {
NotFound ( ) bool
Error ( ) string
}
2022-12-06 15:56:54 +00:00
type notFoundErr struct {
msg string
Add UUID to Fleet errors and clean up error msgs (#10411)
#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)).~
2023-03-13 16:44:06 +00:00
fleet . ErrorWithUUID
2022-12-06 15:56:54 +00:00
}
2018-05-08 02:07:00 +00:00
func ( e notFoundErr ) Error ( ) string {
2022-12-06 15:56:54 +00:00
if e . msg != "" {
return e . msg
}
2018-05-08 02:07:00 +00:00
return "The resource was not found"
}
2021-08-24 17:35:03 +00:00
func ( e notFoundErr ) NotFound ( ) bool {
2018-05-08 02:07:00 +00:00
return true
}
2018-05-09 23:54:23 +00:00
2022-02-28 12:34:44 +00:00
// Implement Is so that errors.Is(err, sql.ErrNoRows) returns true for an
// error of type *notFoundError, without having to wrap sql.ErrNoRows
// explicitly.
func ( e notFoundErr ) Is ( other error ) bool {
return other == sql . ErrNoRows
}
2022-12-06 15:56:54 +00:00
type ConflictErr interface {
Conflict ( ) bool
Error ( ) string
}
type conflictErr struct {
msg string
}
func ( e conflictErr ) Error ( ) string {
return e . msg
}
func ( e conflictErr ) Conflict ( ) bool {
return true
}
2018-05-09 23:54:23 +00:00
type serverError struct {
Message string ` json:"message" `
Errors [ ] struct {
Name string ` json:"name" `
Reason string ` json:"reason" `
} ` json:"errors" `
}
2025-09-19 22:00:19 +00:00
// truncateAndDetectHTML truncates a response body to a reasonable length and
// detects if it's HTML content. Returns the truncated body and whether it's HTML.
func truncateAndDetectHTML ( body [ ] byte , maxLen int ) ( truncated [ ] byte , isHTML bool ) {
if len ( body ) > maxLen {
// Use append which is more idiomatic and efficient
truncated = append ( [ ] byte ( nil ) , body [ : maxLen ] ... )
truncated = append ( truncated , "..." ... )
} else {
// For small bodies, we can return the slice directly since it will be
// converted to string soon anyway and won't hold a large underlying array
truncated = body
}
lowerPrefix := bytes . ToLower ( truncated )
isHTML = bytes . Contains ( lowerPrefix , [ ] byte ( "<html" ) ) || bytes . Contains ( lowerPrefix , [ ] byte ( "<!doctype" ) )
// Return truncated byte slice
return truncated , isHTML
}
2018-05-09 23:54:23 +00:00
func extractServerErrorText ( body io . Reader ) string {
2024-12-31 00:46:42 +00:00
_ , reason := extractServerErrorNameReason ( body )
return reason
}
func extractServerErrorNameReason ( body io . Reader ) ( string , string ) {
2025-09-19 22:00:19 +00:00
// Read the body first so we can try to parse it as JSON and fallback to text if needed
bodyBytes , err := io . ReadAll ( body )
if err != nil {
return "" , "failed to read response body"
}
// Try to parse as JSON first
2018-05-09 23:54:23 +00:00
var serverErr serverError
2025-09-19 22:00:19 +00:00
if err := json . Unmarshal ( bodyBytes , & serverErr ) ; err != nil {
// If it's not JSON, it might be HTML or plain text error from a proxy/load balancer
const maxLen = 200
truncatedBytes , isHTML := truncateAndDetectHTML ( bodyBytes , maxLen )
if isHTML {
// Generic HTML response
return "" , fmt . Sprintf ( "server returned HTML instead of JSON response, body: %s" , truncatedBytes )
}
// Return cleaned up text for non-HTML responses
truncated := strings . TrimSpace ( string ( truncatedBytes ) )
if truncated == "" {
return "" , "empty response body"
}
return "" , truncated
2018-05-09 23:54:23 +00:00
}
2024-12-31 00:46:42 +00:00
errName := ""
errReason := serverErr . Message
2018-05-09 23:54:23 +00:00
if len ( serverErr . Errors ) > 0 {
2024-12-31 00:46:42 +00:00
errReason += ": " + serverErr . Errors [ 0 ] . Reason
errName = serverErr . Errors [ 0 ] . Name
}
return errName , errReason
}
func extractServerErrorNameReasons ( body io . Reader ) ( [ ] string , [ ] string ) {
var serverErr serverError
if err := json . NewDecoder ( body ) . Decode ( & serverErr ) ; err != nil {
return [ ] string { "" } , [ ] string { "unknown" }
}
var errName [ ] string
var errReason [ ] string
for _ , err := range serverErr . Errors {
errName = append ( errName , err . Name )
errReason = append ( errReason , err . Reason )
2018-05-09 23:54:23 +00:00
}
2024-12-31 00:46:42 +00:00
return errName , errReason
2018-05-09 23:54:23 +00:00
}
2023-04-03 18:25:49 +00:00
type statusCodeErr struct {
code int
body string
}
func ( e * statusCodeErr ) Error ( ) string {
return fmt . Sprintf ( "%d %s" , e . code , e . body )
}
func ( e * statusCodeErr ) StatusCode ( ) int {
return e . code
}