2026-03-26 13:59:42 +00:00
package client
2018-05-04 16:53:21 +00:00
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" )
End-user authentication for Window/Linux setup experience: agent (#34847)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34528
# Details
This PR implements the agent changes for allowing Fleet admins to
require that users authenticate with an IdP prior to having their
devices set up. I'll comment on changes inline but the high-level is:
1. Orbit calls the enroll endpoint as usual. This is triggered lazily by
any one of a number of subsystems like device token rotation or
requesting Fleet config
2. If the enroll endpoint returns the new `ErrEndUserAuthRequired`
response, then it opens a window to the `/mdm/sso` Fleet page and
retries the enroll endpoint every 30 seconds indefinitely.
3. Any other non-200 response to the enroll request is treated as before
(limited # of retries, with backoff)
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-
changes.md#changes-files) for more information.
Will add changelog when story is one.
## Testing
- [X] Added/updated automated tests
Added test for new retry logic
- [X] QA'd all new/changed functionality manually
This is kinda hard to test without the associated backend PR:
https://github.com/fleetdm/fleet/pull/34835
## fleetd/orbit/Fleet Desktop
- [X] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
This is compatible with all Fleet versions, since older ones won't send
the new error.
- [X] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
This is compatible with all platforms, although it currently should only
ever run on Windows and Linux since macOS devices will have end-user
auth taken care of before they even download Orbit.
- [ ] Verified that fleetd runs on macOS, Linux and Windows
Testing this now.
- [ ] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added SSO (Single Sign-On) enrollment support for end-user
authentication
* Enhanced error messaging for authentication-required scenarios
* **Bug Fixes**
* Improved error handling and retry logic for enrollment failures
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-03 22:41:57 +00:00
// ErrEndUserAuthRequired is returned when an action (such as enrolling a device)
// requires end user authentication
ErrEndUserAuthRequired = errors . New ( "end user authentication required" )
2022-02-14 16:43:34 +00:00
)
2018-05-04 16:53:21 +00:00
type SetupAlreadyErr interface {
SetupAlready ( ) bool
Error ( ) string
}
2026-03-26 13:59:42 +00:00
type SetupAlreadyError struct { }
2018-05-04 16:53:21 +00:00
2026-03-26 13:59:42 +00:00
func ( e SetupAlreadyError ) Error ( ) string {
2021-01-28 15:57:32 +00:00
return "Fleet has already been setup"
2018-05-04 16:53:21 +00:00
}
2026-03-26 13:59:42 +00:00
func ( e SetupAlreadyError ) SetupAlready ( ) bool {
2018-05-04 16:53:21 +00:00
return true
}
type NotSetupErr interface {
NotSetup ( ) bool
Error ( ) string
}
2026-03-26 13:59:42 +00:00
type NotSetupError struct { }
2018-05-04 16:53:21 +00:00
2026-03-26 13:59:42 +00:00
func ( e NotSetupError ) 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
}
2026-03-26 13:59:42 +00:00
func ( e NotSetupError ) NotSetup ( ) bool {
2018-05-04 16:53:21 +00:00
return true
}
2018-05-08 02:07:00 +00:00
2026-03-26 13:59:42 +00:00
// NotFoundErrIface is the interface for not-found errors.
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.
2026-03-26 13:59:42 +00:00
type NotFoundErrIface interface {
2018-05-08 02:07:00 +00:00
NotFound ( ) bool
Error ( ) string
}
2026-03-26 13:59:42 +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
2026-03-26 13:59:42 +00:00
func ( e * NotFoundErr ) Error ( ) string {
if e . Msg != "" {
return e . Msg
2022-12-06 15:56:54 +00:00
}
2018-05-08 02:07:00 +00:00
return "The resource was not found"
}
2026-03-26 13:59:42 +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
2026-03-26 13:59:42 +00:00
// error of type *NotFoundErr, without having to wrap sql.ErrNoRows
// explicitly. It also matches other *NotFoundErr targets so that pointer-based
2026-03-05 15:17:51 +00:00
// comparison works (pointers to distinct structs are never == even if their
// contents are identical).
2026-03-26 13:59:42 +00:00
func ( e * NotFoundErr ) Is ( other error ) bool {
2026-03-05 15:17:51 +00:00
if other == sql . ErrNoRows {
return true
}
2026-03-26 13:59:42 +00:00
_ , ok := other . ( * NotFoundErr )
2026-03-05 15:17:51 +00:00
return ok
}
2026-03-26 13:59:42 +00:00
// IsNotFoundErr reports whether err's chain contains a *NotFoundErr.
func IsNotFoundErr ( err error ) bool {
var nfe * NotFoundErr
2026-03-05 15:17:51 +00:00
return errors . As ( err , & nfe )
2022-02-28 12:34:44 +00:00
}
2022-12-06 15:56:54 +00:00
type ConflictErr interface {
Conflict ( ) bool
Error ( ) string
}
2026-03-26 13:59:42 +00:00
type ConflictError struct {
Msg string
2022-12-06 15:56:54 +00:00
}
2026-03-26 13:59:42 +00:00
func ( e ConflictError ) Error ( ) string {
return e . Msg
2022-12-06 15:56:54 +00:00
}
2026-03-26 13:59:42 +00:00
func ( e ConflictError ) Conflict ( ) bool {
2022-12-06 15:56:54 +00:00
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" `
}
2026-03-26 13:59:42 +00:00
// TruncateAndDetectHTML truncates a response body to a reasonable length and
2025-09-19 22:00:19 +00:00
// detects if it's HTML content. Returns the truncated body and whether it's HTML.
2026-03-26 13:59:42 +00:00
func TruncateAndDetectHTML ( body [ ] byte , maxLen int ) ( truncated [ ] byte , isHTML bool ) {
2025-09-19 22:00:19 +00:00
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
}
2026-03-26 13:59:42 +00:00
func ExtractServerErrorText ( body io . Reader ) string {
_ , reason := ExtractServerErrorNameReason ( body )
2024-12-31 00:46:42 +00:00
return reason
}
2026-03-26 13:59:42 +00:00
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
2026-03-26 13:59:42 +00:00
truncatedBytes , isHTML := TruncateAndDetectHTML ( bodyBytes , maxLen )
2025-09-19 22:00:19 +00:00
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
}
2026-03-26 13:59:42 +00:00
func ExtractServerErrorNameReasons ( body io . Reader ) ( [ ] string , [ ] string ) {
2024-12-31 00:46:42 +00:00
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
2026-03-26 13:59:42 +00:00
type StatusCodeErr struct {
Code int
Body string
2023-04-03 18:25:49 +00:00
}
2026-03-26 13:59:42 +00:00
func ( e * StatusCodeErr ) Error ( ) string {
return fmt . Sprintf ( "%d %s" , e . Code , e . Body )
2023-04-03 18:25:49 +00:00
}
2026-03-26 13:59:42 +00:00
func ( e * StatusCodeErr ) StatusCode ( ) int {
return e . Code
2023-04-03 18:25:49 +00:00
}