2026-01-06 20:23:07 +00:00
|
|
|
package endpointer
|
2025-02-13 20:32:19 +00:00
|
|
|
|
|
|
|
|
import (
|
2025-02-18 17:09:43 +00:00
|
|
|
"bufio"
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
"bytes"
|
2025-02-18 17:09:43 +00:00
|
|
|
"compress/gzip"
|
2025-02-13 20:32:19 +00:00
|
|
|
"context"
|
Add CSP to fleet(currently disabled - needs frontend work) (#41395)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40538
This is the initial iteration of CSP functionality, currently gated
behind FLEET_SERVER_ENABLE_CSP. If disabled, no CSP is served. Nonces
are still injected into pages however a dummy nonce is used and has no
effect.
With this setting turned on things break and will be addressed by mainly
frontend changes in https://github.com/fleetdm/fleet/issues/41577
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
2026-03-12 22:06:54 +00:00
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/base64"
|
2025-02-13 20:32:19 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2025-02-18 17:09:43 +00:00
|
|
|
"io"
|
2026-02-18 16:09:05 +00:00
|
|
|
"log/slog"
|
2025-02-13 20:32:19 +00:00
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"reflect"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
"sync"
|
2025-03-20 14:09:57 +00:00
|
|
|
"time"
|
2025-02-13 20:32:19 +00:00
|
|
|
|
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2025-02-18 17:09:43 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
2025-03-20 14:09:57 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
2025-12-31 15:12:00 +00:00
|
|
|
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
|
2026-02-24 05:09:08 +00:00
|
|
|
platform_logging "github.com/fleetdm/fleet/v4/server/platform/logging"
|
2026-01-06 20:23:07 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/platform/middleware/authzcheck"
|
|
|
|
|
"github.com/fleetdm/fleet/v4/server/platform/middleware/ratelimit"
|
2025-02-18 17:09:43 +00:00
|
|
|
"github.com/go-kit/kit/endpoint"
|
2025-02-13 20:32:19 +00:00
|
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type HandlerRoutesFunc func(r *mux.Router, opts []kithttp.ServerOption)
|
|
|
|
|
|
|
|
|
|
// ParseTag parses a `url` tag and whether it's optional or not, which is an optional part of the tag
|
|
|
|
|
func ParseTag(tag string) (string, bool, error) {
|
|
|
|
|
parts := strings.Split(tag, ",")
|
|
|
|
|
switch len(parts) {
|
|
|
|
|
case 0:
|
|
|
|
|
return "", false, fmt.Errorf("Error parsing %s: too few parts", tag)
|
|
|
|
|
case 1:
|
|
|
|
|
return tag, false, nil
|
|
|
|
|
case 2:
|
|
|
|
|
return parts[0], parts[1] == "optional", nil
|
|
|
|
|
default:
|
|
|
|
|
return "", false, fmt.Errorf("Error parsing %s: too many parts", tag)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-24 20:31:21 +00:00
|
|
|
type fieldPair struct {
|
2025-02-13 20:32:19 +00:00
|
|
|
Sf reflect.StructField
|
|
|
|
|
V reflect.Value
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-24 20:31:21 +00:00
|
|
|
// allFields returns all the fields for a struct, including the ones from embedded structs
|
|
|
|
|
func allFields(ifv reflect.Value) []fieldPair {
|
2025-02-13 20:32:19 +00:00
|
|
|
if ifv.Kind() == reflect.Ptr {
|
|
|
|
|
ifv = ifv.Elem()
|
|
|
|
|
}
|
|
|
|
|
if ifv.Kind() != reflect.Struct {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-24 20:31:21 +00:00
|
|
|
var fields []fieldPair
|
2025-02-13 20:32:19 +00:00
|
|
|
|
|
|
|
|
if !ifv.IsValid() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t := ifv.Type()
|
|
|
|
|
|
|
|
|
|
for i := 0; i < ifv.NumField(); i++ {
|
|
|
|
|
v := ifv.Field(i)
|
|
|
|
|
|
|
|
|
|
if v.Kind() == reflect.Struct && t.Field(i).Anonymous {
|
2025-02-24 20:31:21 +00:00
|
|
|
fields = append(fields, allFields(v)...)
|
2025-02-13 20:32:19 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2025-02-24 20:31:21 +00:00
|
|
|
fields = append(fields, fieldPair{Sf: ifv.Type().Field(i), V: v})
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fields
|
|
|
|
|
}
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// aliasRulesCache caches the result of ExtractAliasRules by reflect.Type so
|
|
|
|
|
// that the reflection walk happens only once per struct type, not on every
|
|
|
|
|
// request.
|
|
|
|
|
var aliasRulesCache sync.Map // reflect.Type → []AliasRule
|
|
|
|
|
|
|
|
|
|
// ExtractAliasRules inspects the struct type of iface (recursively, including
|
|
|
|
|
// embedded structs) and builds an []AliasRule from fields that carry a
|
|
|
|
|
// `renameto` struct tag. For each such field the json tag's field name
|
|
|
|
|
// becomes OldKey (the current/deprecated name) and the renameto value becomes
|
|
|
|
|
// NewKey (the target name).
|
|
|
|
|
//
|
|
|
|
|
// Only `json` tags are considered; `url` and `query` tags are ignored for now.
|
|
|
|
|
//
|
|
|
|
|
// The returned slice is deduplicated: if the same alias pair appears on
|
|
|
|
|
// multiple fields (e.g. in both a request and an embedded struct) it is
|
|
|
|
|
// included only once.
|
|
|
|
|
//
|
|
|
|
|
// Results are cached by type so that the reflection walk only happens once.
|
|
|
|
|
func ExtractAliasRules(iface any) []AliasRule {
|
|
|
|
|
if iface == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
t := reflect.TypeOf(iface)
|
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cached, ok := aliasRulesCache.Load(t); ok {
|
|
|
|
|
return cached.([]AliasRule)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen := make(map[AliasRule]bool)
|
|
|
|
|
var rules []AliasRule
|
|
|
|
|
extractAliasRulesFromType(t, seen, &rules)
|
|
|
|
|
aliasRulesCache.Store(t, rules)
|
|
|
|
|
return rules
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractAliasRulesFromType(t reflect.Type, seen map[AliasRule]bool, rules *[]AliasRule) {
|
|
|
|
|
// visited tracks types we've already walked to avoid infinite recursion
|
|
|
|
|
// from cyclic type references (e.g. type Node struct { Children []Node }).
|
|
|
|
|
visited := make(map[reflect.Type]bool)
|
|
|
|
|
extractAliasRulesRecursive(t, seen, rules, visited)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elemType dereferences pointer, slice, array, and map types to find the
|
|
|
|
|
// underlying (possibly struct) element type.
|
|
|
|
|
func elemType(t reflect.Type) reflect.Type {
|
|
|
|
|
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Array || t.Kind() == reflect.Map {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recursively extract alias rules from the type t.
|
|
|
|
|
// This should only be called on struct types.
|
|
|
|
|
func extractAliasRulesRecursive(t reflect.Type, seen map[AliasRule]bool, rules *[]AliasRule, visited map[reflect.Type]bool) {
|
|
|
|
|
if visited[t] {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
visited[t] = true
|
|
|
|
|
|
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
|
structField := t.Field(i)
|
|
|
|
|
|
|
|
|
|
// Check this field for a renameto tag.
|
|
|
|
|
renameTo, hasRenameTo := structField.Tag.Lookup("renameto")
|
|
|
|
|
if hasRenameTo && renameTo != "" {
|
|
|
|
|
jsonTag, hasJSON := structField.Tag.Lookup("json")
|
|
|
|
|
if hasJSON && jsonTag != "" && jsonTag != "-" {
|
|
|
|
|
// Strip options like ",omitempty" from the json tag.
|
|
|
|
|
jsonFieldName, _, _ := strings.Cut(jsonTag, ",")
|
|
|
|
|
if jsonFieldName != "" && jsonFieldName != "-" {
|
|
|
|
|
rule := AliasRule{OldKey: jsonFieldName, NewKey: renameTo}
|
|
|
|
|
if !seen[rule] {
|
|
|
|
|
seen[rule] = true
|
|
|
|
|
*rules = append(*rules, rule)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recurse into any struct type reachable from this field
|
|
|
|
|
// (through pointers, slices, arrays, maps, or directly).
|
|
|
|
|
fieldType := elemType(structField.Type)
|
|
|
|
|
if fieldType.Kind() == reflect.Struct {
|
|
|
|
|
extractAliasRulesRecursive(fieldType, seen, rules, visited)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 20:32:19 +00:00
|
|
|
func BadRequestErr(publicMsg string, internalErr error) error {
|
|
|
|
|
// ensure timeout errors don't become BadRequestErrors.
|
|
|
|
|
var opErr *net.OpError
|
|
|
|
|
if errors.As(internalErr, &opErr) {
|
|
|
|
|
return fmt.Errorf(publicMsg+", internal: %w", internalErr)
|
|
|
|
|
}
|
2025-12-31 15:12:00 +00:00
|
|
|
return &platform_http.BadRequestError{
|
2025-02-13 20:32:19 +00:00
|
|
|
Message: publicMsg,
|
|
|
|
|
InternalErr: internalErr,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func UintFromRequest(r *http.Request, name string) (uint64, error) {
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
s, ok := vars[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, ErrBadRoute
|
|
|
|
|
}
|
|
|
|
|
u, err := strconv.ParseUint(s, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, ctxerr.Wrap(r.Context(), err, "UintFromRequest")
|
|
|
|
|
}
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IntFromRequest(r *http.Request, name string) (int64, error) {
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
s, ok := vars[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return 0, ErrBadRoute
|
|
|
|
|
}
|
|
|
|
|
u, err := strconv.ParseInt(s, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, ctxerr.Wrap(r.Context(), err, "IntFromRequest")
|
|
|
|
|
}
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func StringFromRequest(r *http.Request, name string) (string, error) {
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
s, ok := vars[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", ErrBadRoute
|
|
|
|
|
}
|
|
|
|
|
unescaped, err := url.PathUnescape(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", ctxerr.Wrap(r.Context(), err, "unescape value in path")
|
|
|
|
|
}
|
|
|
|
|
return unescaped, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func DecodeURLTagValue(r *http.Request, field reflect.Value, urlTagValue string, optional bool) error {
|
|
|
|
|
switch field.Kind() {
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
v, err := IntFromRequest(r, urlTagValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, ErrBadRoute) && optional {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return BadRequestErr("IntFromRequest", err)
|
|
|
|
|
}
|
|
|
|
|
field.SetInt(v)
|
|
|
|
|
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
v, err := UintFromRequest(r, urlTagValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, ErrBadRoute) && optional {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return BadRequestErr("UintFromRequest", err)
|
|
|
|
|
}
|
|
|
|
|
field.SetUint(v)
|
|
|
|
|
|
|
|
|
|
case reflect.String:
|
|
|
|
|
v, err := StringFromRequest(r, urlTagValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, ErrBadRoute) && optional {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return BadRequestErr("StringFromRequest", err)
|
|
|
|
|
}
|
|
|
|
|
field.SetString(v)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("unsupported type for field %s for 'url' decoding: %s", urlTagValue, field.Kind())
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
// DomainQueryFieldDecoder decodes a query parameter value into the target field.
|
|
|
|
|
// It returns true if it handled the field, false if default handling should be used.
|
|
|
|
|
type DomainQueryFieldDecoder func(queryTagName, queryVal string, field reflect.Value) (handled bool, err error)
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
func DecodeQueryTagValue(r *http.Request, fp fieldPair, customDecoder DomainQueryFieldDecoder, ctx context.Context) error {
|
2025-02-13 20:32:19 +00:00
|
|
|
queryTagValue, ok := fp.Sf.Tag.Lookup("query")
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
var err error
|
|
|
|
|
var optional bool
|
|
|
|
|
queryTagValue, optional, err = ParseTag(queryTagValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
queryVal := r.URL.Query().Get(queryTagValue)
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
|
|
|
|
|
// The query tag now holds the old (deprecated) name. If the old name
|
|
|
|
|
// was used, log a deprecation warning. If not found, check the
|
|
|
|
|
// renameto value (the new name) as a fallback.
|
|
|
|
|
if queryVal != "" {
|
|
|
|
|
if renameTo, hasRenameTo := fp.Sf.Tag.Lookup("renameto"); hasRenameTo {
|
|
|
|
|
// Check for conflict: if both old and new names are provided, return an error.
|
|
|
|
|
newName, _, _ := ParseTag(renameTo)
|
|
|
|
|
if newVal := r.URL.Query().Get(newName); newVal != "" {
|
|
|
|
|
return &platform_http.BadRequestError{
|
|
|
|
|
Message: fmt.Sprintf("Specify only one of %q or %q", queryTagValue, newName),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Log deprecation warning - the old name was used.
|
2026-02-24 05:09:08 +00:00
|
|
|
if platform_logging.TopicEnabled(platform_logging.DeprecatedFieldTopic) {
|
|
|
|
|
logging.WithLevel(ctx, slog.LevelWarn)
|
|
|
|
|
logging.WithExtras(ctx,
|
|
|
|
|
"deprecated_param", queryTagValue,
|
|
|
|
|
"deprecation_warning", fmt.Sprintf("'%s' is deprecated, use '%s' instead", queryTagValue, renameTo),
|
|
|
|
|
)
|
|
|
|
|
}
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
}
|
|
|
|
|
} else if renameTo, hasRenameTo := fp.Sf.Tag.Lookup("renameto"); hasRenameTo {
|
|
|
|
|
renameTo, _, err = ParseTag(renameTo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
queryVal = r.URL.Query().Get(renameTo)
|
|
|
|
|
}
|
|
|
|
|
// If we still don't have a value, return if this is optional, otherwise error.
|
2025-02-13 20:32:19 +00:00
|
|
|
if queryVal == "" {
|
|
|
|
|
if optional {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-31 15:12:00 +00:00
|
|
|
return &platform_http.BadRequestError{Message: fmt.Sprintf("Param %s is required", queryTagValue)}
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
field := fp.V
|
|
|
|
|
if field.Kind() == reflect.Ptr {
|
|
|
|
|
// create the new instance of whatever it is
|
|
|
|
|
field.Set(reflect.New(field.Type().Elem()))
|
|
|
|
|
field = field.Elem()
|
|
|
|
|
}
|
2025-12-31 15:12:00 +00:00
|
|
|
|
|
|
|
|
// Try custom decoder first if provided
|
|
|
|
|
if customDecoder != nil {
|
|
|
|
|
handled, err := customDecoder(queryTagValue, queryVal, field)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if handled {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 20:32:19 +00:00
|
|
|
switch field.Kind() {
|
|
|
|
|
case reflect.String:
|
|
|
|
|
field.SetString(queryVal)
|
|
|
|
|
case reflect.Uint:
|
|
|
|
|
queryValUint, err := strconv.Atoi(queryVal)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return BadRequestErr("parsing uint from query", err)
|
|
|
|
|
}
|
|
|
|
|
field.SetUint(uint64(queryValUint)) //nolint:gosec // dismiss G115
|
|
|
|
|
case reflect.Float64:
|
|
|
|
|
queryValFloat, err := strconv.ParseFloat(queryVal, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return BadRequestErr("parsing float from query", err)
|
|
|
|
|
}
|
|
|
|
|
field.SetFloat(queryValFloat)
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
field.SetBool(queryVal == "1" || queryVal == "true")
|
|
|
|
|
case reflect.Int:
|
2025-12-31 15:12:00 +00:00
|
|
|
queryValInt, err := strconv.Atoi(queryVal)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return BadRequestErr("parsing int from query", err)
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
field.SetInt(int64(queryValInt))
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("Cant handle type for field %s %s", fp.Sf.Name, field.Kind())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// copied from https://github.com/go-chi/chi/blob/c97bc988430d623a14f50b7019fb40529036a35a/middleware/realip.go#L42
|
2025-03-20 14:09:57 +00:00
|
|
|
var (
|
|
|
|
|
trueClientIP = http.CanonicalHeaderKey("True-Client-IP")
|
|
|
|
|
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
|
|
|
|
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
|
|
|
|
)
|
2025-02-13 20:32:19 +00:00
|
|
|
|
2026-01-20 04:13:37 +00:00
|
|
|
func extractIP(r *http.Request) string {
|
2025-02-13 20:32:19 +00:00
|
|
|
ip := r.RemoteAddr
|
|
|
|
|
if i := strings.LastIndexByte(ip, ':'); i != -1 {
|
|
|
|
|
ip = ip[:i]
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 13:50:42 +00:00
|
|
|
// Prefer True-Client-IP and X-Real-IP headers before X-Forwarded-For:
|
|
|
|
|
// - True-Client-IP: set by some CDNs (e.g., Akamai) to indicate the real client IP early in the chain
|
|
|
|
|
// - X-Real-IP: set by Nginx or similar proxies as a simpler alternative to X-Forwarded-For
|
|
|
|
|
// These headers are less likely to be spoofed or malformed compared to X-Forwarded-For.
|
2025-02-13 20:32:19 +00:00
|
|
|
if tcip := r.Header.Get(trueClientIP); tcip != "" {
|
|
|
|
|
ip = tcip
|
|
|
|
|
} else if xrip := r.Header.Get(xRealIP); xrip != "" {
|
|
|
|
|
ip = xrip
|
|
|
|
|
} else if xff := r.Header.Get(xForwardedFor); xff != "" {
|
2025-06-27 13:50:42 +00:00
|
|
|
// X-Forwarded-For is a comma-separated list of IP addresses representing the chain of proxies
|
|
|
|
|
// that a request has passed through. This is not a standard, but a convention.
|
|
|
|
|
// The convention is to treat the left-most IP address as the original client IP.
|
|
|
|
|
// For example:
|
|
|
|
|
// X-Forwarded-For: 198.51.100.1, 203.0.113.5, 127.0.0.1
|
|
|
|
|
// Means:
|
|
|
|
|
// - 198.51.100.1 is the client IP
|
|
|
|
|
// - 127.0.0.1 is the last proxy (likely this server or a local proxy)
|
|
|
|
|
//
|
|
|
|
|
// If the left-most IP is a private or loopback address (e.g., 127.0.0.1 or 10.x.x.x), it may indicate:
|
|
|
|
|
// - The request originated from a local proxy, or
|
|
|
|
|
// - The header was spoofed by a client (untrusted source)
|
|
|
|
|
//
|
|
|
|
|
// Having multiple X-Forwarded-For headers is non-standard, so we do not handle it here.
|
|
|
|
|
//
|
|
|
|
|
// Here, we grab the left-most IP address by convention.
|
2025-02-13 20:32:19 +00:00
|
|
|
i := strings.Index(xff, ",")
|
|
|
|
|
if i == -1 {
|
|
|
|
|
i = len(xff)
|
|
|
|
|
}
|
|
|
|
|
ip = xff[:i]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ErrorHandler struct {
|
2026-02-18 16:09:05 +00:00
|
|
|
Logger *slog.Logger
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ErrorHandler) Handle(ctx context.Context, err error) {
|
|
|
|
|
path, _ := ctx.Value(kithttp.ContextKeyRequestPath).(string)
|
2026-02-18 16:09:05 +00:00
|
|
|
|
|
|
|
|
attrs := []any{"path", path}
|
2025-02-13 20:32:19 +00:00
|
|
|
|
2025-03-20 14:09:57 +00:00
|
|
|
if startTime, ok := logging.StartTime(ctx); ok && !startTime.IsZero() {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "took", time.Since(startTime))
|
2025-03-20 14:09:57 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
var ewi platform_http.ErrWithInternal
|
2025-02-13 20:32:19 +00:00
|
|
|
if errors.As(err, &ewi) {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "internal", ewi.Internal())
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
var ewlf platform_http.ErrWithLogFields
|
2025-02-13 20:32:19 +00:00
|
|
|
if errors.As(err, &ewlf) {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, ewlf.LogFields()...)
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
var uuider platform_http.ErrorUUIDer
|
2025-02-13 20:32:19 +00:00
|
|
|
if errors.As(err, &uuider) {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "uuid", uuider.UUID())
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rle ratelimit.Error
|
|
|
|
|
if errors.As(err, &rle) {
|
|
|
|
|
res := rle.Result()
|
2025-09-26 18:03:50 +00:00
|
|
|
if res.RetryAfter > 0 {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "retry_after", res.RetryAfter)
|
2025-09-26 18:03:50 +00:00
|
|
|
}
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "err", "limit exceeded")
|
2025-02-13 20:32:19 +00:00
|
|
|
} else {
|
2026-02-18 16:09:05 +00:00
|
|
|
attrs = append(attrs, "err", err)
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
2026-02-18 16:09:05 +00:00
|
|
|
|
|
|
|
|
h.Logger.InfoContext(ctx, "request error", attrs...)
|
2025-02-13 20:32:19 +00:00
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
|
2025-02-24 20:31:21 +00:00
|
|
|
// A value that implements RequestDecoder takes control of decoding the request
|
2025-02-18 17:09:43 +00:00
|
|
|
// as a whole - that is, it is responsible for decoding the body and any url
|
|
|
|
|
// or query argument itself.
|
2025-02-24 20:31:21 +00:00
|
|
|
type RequestDecoder interface {
|
2025-02-18 17:09:43 +00:00
|
|
|
DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-25 15:42:17 +00:00
|
|
|
// A value that implements requestValidator is called after having the values
|
|
|
|
|
// decoded into it to apply further validations.
|
|
|
|
|
type requestValidator interface {
|
|
|
|
|
ValidateRequest() error
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
// MakeDecoder creates a decoder for the type for the struct passed on. If the
|
2025-12-31 15:12:00 +00:00
|
|
|
// struct has at least 1 json tag it'll unmarshall the body. Custom `url` tag
|
|
|
|
|
// values can be handled by providing a parseCustomTags function. Note that
|
|
|
|
|
// these behaviors do not work for embedded structs.
|
2025-02-18 17:09:43 +00:00
|
|
|
//
|
2025-12-31 15:12:00 +00:00
|
|
|
// Any other `url` tag will be treated as a path variable (of the form
|
2025-02-18 17:09:43 +00:00
|
|
|
// /path/{name} in the route's path) from the URL path pattern, and it'll be
|
|
|
|
|
// decoded and set accordingly. Variables can be optional by setting the tag as
|
|
|
|
|
// follows: `url:"some-id,optional"`.
|
|
|
|
|
//
|
2025-02-24 20:31:21 +00:00
|
|
|
// If iface implements the RequestDecoder interface, it returns a function that
|
2025-02-18 17:09:43 +00:00
|
|
|
// calls iface.DecodeRequest(ctx, r) - i.e. the value itself fully controls its
|
|
|
|
|
// own decoding.
|
|
|
|
|
//
|
|
|
|
|
// If iface implements the bodyDecoder interface, it calls iface.DecodeBody
|
|
|
|
|
// after having decoded any non-body fields (such as url and query parameters)
|
|
|
|
|
// into the struct.
|
2025-12-31 15:12:00 +00:00
|
|
|
//
|
|
|
|
|
// The customQueryDecoder parameter allows services to inject domain-specific
|
|
|
|
|
// query parameter decoding logic.
|
2026-02-05 15:29:53 +00:00
|
|
|
//
|
2026-03-25 09:27:58 +00:00
|
|
|
// If adding a new way to parse/decode the request, make sure to wrap the body with http.MaxBytesReader using the maxRequestBodySize
|
2026-03-09 17:49:07 +00:00
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
func MakeDecoder(
|
|
|
|
|
iface interface{},
|
|
|
|
|
jsonUnmarshal func(body io.Reader, req any) error,
|
|
|
|
|
parseCustomTags func(urlTagValue string, r *http.Request, field reflect.Value) (bool, error),
|
|
|
|
|
isBodyDecoder func(reflect.Value) bool,
|
|
|
|
|
decodeBody func(ctx context.Context, r *http.Request, v reflect.Value, body io.Reader) error,
|
2025-12-31 15:12:00 +00:00
|
|
|
customQueryDecoder DomainQueryFieldDecoder,
|
2026-02-05 15:29:53 +00:00
|
|
|
maxRequestBodySize int64,
|
2025-02-18 17:09:43 +00:00
|
|
|
) kithttp.DecodeRequestFunc {
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// Infer alias rules from `renameto` struct tags on the request type.
|
|
|
|
|
aliasRules := ExtractAliasRules(iface)
|
2025-02-18 17:09:43 +00:00
|
|
|
if iface == nil {
|
|
|
|
|
return func(ctx context.Context, r *http.Request) (interface{}, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-24 20:31:21 +00:00
|
|
|
if rd, ok := iface.(RequestDecoder); ok {
|
2025-02-18 17:09:43 +00:00
|
|
|
return func(ctx context.Context, r *http.Request) (interface{}, error) {
|
2026-02-05 15:29:53 +00:00
|
|
|
if maxRequestBodySize != -1 {
|
2026-03-25 09:27:58 +00:00
|
|
|
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestBodySize)
|
|
|
|
|
}
|
|
|
|
|
//
|
|
|
|
|
// We take care of gzip encoding here to prevent any future DecodeRequest
|
|
|
|
|
// implementations from missing gzip bomb checks.
|
|
|
|
|
//
|
|
|
|
|
gzipped := false
|
|
|
|
|
if strings.EqualFold(r.Header.Get("content-encoding"), "gzip") {
|
|
|
|
|
gzipped = true
|
|
|
|
|
gzr, err := gzip.NewReader(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, BadRequestErr("gzip decoder error", err)
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
2026-03-25 09:27:58 +00:00
|
|
|
defer gzr.Close()
|
|
|
|
|
if maxRequestBodySize != -1 {
|
|
|
|
|
// Limit decompressed bytes to prevent gzip bombs from bypassing
|
|
|
|
|
// the raw body size limit applied above.
|
|
|
|
|
r.Body = http.MaxBytesReader(nil, gzr, maxRequestBodySize)
|
|
|
|
|
} else {
|
|
|
|
|
r.Body = io.NopCloser(gzr)
|
|
|
|
|
}
|
|
|
|
|
// Clear so implementations don't try to decompress again.
|
|
|
|
|
r.Header.Del("Content-Encoding")
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
|
|
|
|
ret, err := rd.DecodeRequest(ctx, r)
|
2026-03-25 09:27:58 +00:00
|
|
|
|
|
|
|
|
// Some DecodeRequest implementations (like getHostSoftwareRequest)
|
|
|
|
|
// themselves return platform_http.PayloadTooLargeError.
|
|
|
|
|
if inner, isPayloadTooLargeError := errors.AsType[platform_http.PayloadTooLargeError](err); isPayloadTooLargeError {
|
|
|
|
|
// Preserve the inner error's MaxRequestSize and ContentLength
|
|
|
|
|
// (it knows the actual limit that was hit), only add Gzipped.
|
|
|
|
|
inner.Gzipped = gzipped
|
|
|
|
|
return nil, inner
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 18:16:35 +00:00
|
|
|
// This is the DecodeRequest implementation returning http.MaxBytesError
|
|
|
|
|
// (e.g. there's a size limit when uploading installers.)
|
2026-03-25 09:27:58 +00:00
|
|
|
if _, isMaxBytesError := errors.AsType[*http.MaxBytesError](err); isMaxBytesError {
|
|
|
|
|
return nil, platform_http.PayloadTooLargeError{
|
|
|
|
|
ContentLength: r.Header.Get("Content-Length"),
|
|
|
|
|
MaxRequestSize: maxRequestBodySize,
|
|
|
|
|
Gzipped: gzipped,
|
|
|
|
|
}
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
|
|
|
|
return ret, err
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t := reflect.TypeOf(iface)
|
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
|
panic(fmt.Sprintf("MakeDecoder only understands structs, not %T", iface))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return func(ctx context.Context, r *http.Request) (interface{}, error) {
|
|
|
|
|
v := reflect.New(t)
|
|
|
|
|
nilBody := false
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
var rewriter *JSONKeyRewriteReader
|
2025-02-18 17:09:43 +00:00
|
|
|
|
2026-02-05 15:29:53 +00:00
|
|
|
if maxRequestBodySize != -1 {
|
2026-03-25 09:27:58 +00:00
|
|
|
r.Body = http.MaxBytesReader(nil, r.Body, maxRequestBodySize)
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
buf := bufio.NewReader(r.Body)
|
|
|
|
|
var body io.Reader = buf
|
2026-03-25 09:27:58 +00:00
|
|
|
gzipped := false
|
2025-02-18 17:09:43 +00:00
|
|
|
if _, err := buf.Peek(1); err == io.EOF {
|
|
|
|
|
nilBody = true
|
|
|
|
|
} else {
|
2026-03-25 09:27:58 +00:00
|
|
|
if strings.EqualFold(r.Header.Get("content-encoding"), "gzip") {
|
|
|
|
|
gzipped = true
|
2025-02-18 17:09:43 +00:00
|
|
|
gzr, err := gzip.NewReader(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, BadRequestErr("gzip decoder error", err)
|
|
|
|
|
}
|
|
|
|
|
defer gzr.Close()
|
2026-03-25 09:27:58 +00:00
|
|
|
if maxRequestBodySize != -1 {
|
|
|
|
|
// Limit decompressed bytes to prevent gzip bombs from bypassing
|
|
|
|
|
// the raw body size limit applied above.
|
|
|
|
|
body = http.MaxBytesReader(nil, gzr, maxRequestBodySize)
|
|
|
|
|
} else {
|
|
|
|
|
body = gzr
|
|
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// Insert the JSON key rewriter into the reader pipeline
|
|
|
|
|
// (after gzip decompression, before JSON decoding) to rename
|
|
|
|
|
// deprecated field names and detect alias conflicts.
|
|
|
|
|
if len(aliasRules) > 0 {
|
|
|
|
|
rewriter = NewJSONKeyRewriteReader(body, aliasRules)
|
|
|
|
|
//nolint:errcheck // nothing to do on .Close() error.
|
|
|
|
|
defer rewriter.Close()
|
|
|
|
|
body = rewriter
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
if isBodyDecoder == nil || !isBodyDecoder(v) {
|
|
|
|
|
req := v.Interface()
|
|
|
|
|
err := jsonUnmarshal(body, req)
|
|
|
|
|
if err != nil {
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// Check for alias conflict errors from the rewriter.
|
|
|
|
|
var ace *AliasConflictError
|
|
|
|
|
if errors.As(err, &ace) {
|
|
|
|
|
return nil, &platform_http.BadRequestError{
|
|
|
|
|
Message: fmt.Sprintf("Specify only one of %q or %q", ace.Old, ace.New),
|
|
|
|
|
InternalErr: ace,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-25 09:27:58 +00:00
|
|
|
if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
|
|
|
|
|
return nil, platform_http.PayloadTooLargeError{
|
|
|
|
|
ContentLength: r.Header.Get("Content-Length"),
|
|
|
|
|
MaxRequestSize: maxRequestBodySize,
|
|
|
|
|
Gzipped: gzipped,
|
|
|
|
|
}
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
return nil, BadRequestErr("json decoder error", err)
|
|
|
|
|
}
|
|
|
|
|
v = reflect.ValueOf(req)
|
|
|
|
|
}
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-24 20:31:21 +00:00
|
|
|
fields := allFields(v)
|
2025-02-18 17:09:43 +00:00
|
|
|
for _, fp := range fields {
|
|
|
|
|
field := fp.V
|
|
|
|
|
|
|
|
|
|
urlTagValue, ok := fp.Sf.Tag.Lookup("url")
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
if ok {
|
|
|
|
|
optional := false
|
|
|
|
|
urlTagValue, optional, err = ParseTag(urlTagValue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
foundValue := false
|
|
|
|
|
if parseCustomTags != nil {
|
|
|
|
|
foundValue, err = parseCustomTags(urlTagValue, r, field)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !foundValue {
|
|
|
|
|
err := DecodeURLTagValue(r, field, urlTagValue, optional)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, jsonExpected := fp.Sf.Tag.Lookup("json")
|
|
|
|
|
if jsonExpected && nilBody {
|
2025-12-31 15:12:00 +00:00
|
|
|
return nil, &platform_http.BadRequestError{Message: "Expected JSON Body"}
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-29 19:26:55 +00:00
|
|
|
isContentJson := r.Header.Get("Content-Type") == "application/json"
|
|
|
|
|
isCrossSite := r.Header.Get("Origin") != "" || r.Header.Get("Referer") != ""
|
|
|
|
|
if jsonExpected && isCrossSite && !isContentJson {
|
2025-12-31 15:12:00 +00:00
|
|
|
return nil, platform_http.NewUserMessageError(errors.New("Expected Content-Type \"application/json\""), http.StatusUnsupportedMediaType)
|
2025-05-29 19:26:55 +00:00
|
|
|
}
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
err = DecodeQueryTagValue(r, fp, customQueryDecoder, ctx)
|
2025-02-18 17:09:43 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isBodyDecoder != nil && isBodyDecoder(v) {
|
|
|
|
|
err := decodeBody(ctx, r, v, body)
|
|
|
|
|
if err != nil {
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// Check for alias conflict errors from the rewriter.
|
|
|
|
|
var ace *AliasConflictError
|
|
|
|
|
if errors.As(err, &ace) {
|
|
|
|
|
return nil, &platform_http.BadRequestError{
|
|
|
|
|
Message: fmt.Sprintf("Specify only one of %q or %q", ace.Old, ace.New),
|
|
|
|
|
InternalErr: ace,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 09:27:58 +00:00
|
|
|
if _, ok := errors.AsType[*http.MaxBytesError](err); ok {
|
|
|
|
|
return nil, platform_http.PayloadTooLargeError{
|
|
|
|
|
ContentLength: r.Header.Get("Content-Length"),
|
|
|
|
|
MaxRequestSize: maxRequestBodySize,
|
|
|
|
|
Gzipped: gzipped,
|
2026-03-09 17:49:07 +00:00
|
|
|
}
|
2026-03-25 09:27:58 +00:00
|
|
|
}
|
|
|
|
|
if errors.Is(err, io.ErrUnexpectedEOF) {
|
2026-03-09 17:49:07 +00:00
|
|
|
return nil, BadRequestErr("json decoder error", err)
|
2026-02-05 15:29:53 +00:00
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
|
2026-03-17 14:59:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log deprecation warnings when deprecated field names are used.
|
|
|
|
|
if rewriter != nil && platform_logging.TopicEnabled(platform_logging.DeprecatedFieldTopic) {
|
|
|
|
|
if deprecated := rewriter.UsedDeprecatedKeys(); len(deprecated) > 0 {
|
|
|
|
|
newNames := make([]string, len(deprecated))
|
|
|
|
|
for i, old := range deprecated {
|
|
|
|
|
for _, rule := range aliasRules {
|
|
|
|
|
if rule.OldKey == old {
|
|
|
|
|
newNames[i] = rule.NewKey
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
}
|
2026-03-17 14:59:03 +00:00
|
|
|
logging.WithLevel(ctx, slog.LevelWarn)
|
|
|
|
|
logging.WithExtras(ctx,
|
|
|
|
|
"deprecated_fields", fmt.Sprintf("%v", deprecated),
|
|
|
|
|
"deprecation_warning", fmt.Sprintf("use the updated field names (%s) instead", newNames),
|
|
|
|
|
)
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !license.IsPremium(ctx) {
|
|
|
|
|
for _, fp := range fields {
|
|
|
|
|
if prem, ok := fp.Sf.Tag.Lookup("premium"); ok {
|
|
|
|
|
val, err := strconv.ParseBool(prem)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if val && !fp.V.IsZero() {
|
2025-12-31 15:12:00 +00:00
|
|
|
return nil, &platform_http.BadRequestError{Message: fmt.Sprintf(
|
2025-02-18 17:09:43 +00:00
|
|
|
"option %s requires a premium license",
|
|
|
|
|
fp.Sf.Name,
|
|
|
|
|
)}
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-25 15:42:17 +00:00
|
|
|
if rv, ok := v.Interface().(requestValidator); ok {
|
|
|
|
|
if err := rv.ValidateRequest(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
return v.Interface(), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add CSP to fleet(currently disabled - needs frontend work) (#41395)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40538
This is the initial iteration of CSP functionality, currently gated
behind FLEET_SERVER_ENABLE_CSP. If disabled, no CSP is served. Nonces
are still injected into pages however a dummy nonce is used and has no
effect.
With this setting turned on things break and will be addressed by mainly
frontend changes in https://github.com/fleetdm/fleet/issues/41577
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
2026-03-12 22:06:54 +00:00
|
|
|
func newNonce() (string, error) {
|
|
|
|
|
b := make([]byte, 16)
|
|
|
|
|
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func WriteBrowserSecurityHeaders(w http.ResponseWriter, serveCSP, includeNonce bool) (string, error) {
|
|
|
|
|
// This endpoint can optionally return a nonce if needed for the Content-Security-Policy header. In general only
|
|
|
|
|
// our HTML responses need the nonce, API and other static assets should not include it. We return an empty but
|
|
|
|
|
// syntactically valid nonce in the unused case since this will still get substituted into HTML templates when unused
|
|
|
|
|
nonce := "disabled"
|
|
|
|
|
nonceExtraParam := ""
|
|
|
|
|
// generate a unique nonce for this response
|
|
|
|
|
if includeNonce {
|
|
|
|
|
var err error
|
|
|
|
|
nonce, err = newNonce()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
nonceExtraParam = fmt.Sprintf(" 'nonce-%s'", nonce)
|
|
|
|
|
}
|
2025-02-18 17:09:43 +00:00
|
|
|
// Strict-Transport-Security informs browsers that the site should only be
|
|
|
|
|
// accessed using HTTPS, and that any future attempts to access it using
|
|
|
|
|
// HTTP should automatically be converted to HTTPS.
|
|
|
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains;")
|
|
|
|
|
// X-Frames-Options disallows embedding the UI in other sites via <frame>,
|
|
|
|
|
// <iframe>, <embed> or <object>, which can prevent attacks like
|
|
|
|
|
// clickjacking.
|
|
|
|
|
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
|
|
|
|
// X-Content-Type-Options prevents browsers from trying to guess the MIME
|
|
|
|
|
// type which can cause browsers to transform non-executable content into
|
|
|
|
|
// executable content.
|
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
// Referrer-Policy prevents leaking the origin of the referrer in the
|
|
|
|
|
// Referer.
|
|
|
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
Add CSP to fleet(currently disabled - needs frontend work) (#41395)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40538
This is the initial iteration of CSP functionality, currently gated
behind FLEET_SERVER_ENABLE_CSP. If disabled, no CSP is served. Nonces
are still injected into pages however a dummy nonce is used and has no
effect.
With this setting turned on things break and will be addressed by mainly
frontend changes in https://github.com/fleetdm/fleet/issues/41577
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
2026-03-12 22:06:54 +00:00
|
|
|
if serveCSP {
|
|
|
|
|
// TODO Is https OK for img-src? We allow customers to upload their own images and we have to reach out to gravatar for others.
|
|
|
|
|
// NB: If default-src ever changes from 'none' make sure to add object-src 'none'
|
|
|
|
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'self'; connect-src 'self' www.gravatar.com ws: wss:; img-src 'self' www.gravatar.com data: https:; style-src 'self'"+nonceExtraParam+"; font-src 'self'; script-src 'self'"+nonceExtraParam)
|
|
|
|
|
}
|
|
|
|
|
return nonce, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BrowserSecurityHeadersHandler(serveCSP bool, h http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// We don't implement the nonce here in the generic case, however the few endpoints that need it should implement
|
|
|
|
|
// their own handling
|
|
|
|
|
_, err := WriteBrowserSecurityHeaders(w, serveCSP, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "failed to write browser security headers", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
|
})
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 04:20:35 +00:00
|
|
|
// handlerKey identifies a registered handler by HTTP method and unversioned path template.
|
|
|
|
|
type handlerKey struct {
|
|
|
|
|
method string
|
|
|
|
|
path string // unversioned path template, e.g. "/api/_version_/fleet/fleets"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandlerRegistry stores HTTP handlers by method+path during endpoint registration,
|
|
|
|
|
// enabling lookup for deprecated path alias registration.
|
|
|
|
|
type HandlerRegistry struct {
|
|
|
|
|
handlers map[handlerKey]http.Handler
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewHandlerRegistry creates an empty HandlerRegistry.
|
|
|
|
|
func NewHandlerRegistry() *HandlerRegistry {
|
|
|
|
|
return &HandlerRegistry{handlers: make(map[handlerKey]http.Handler)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeprecatedPathAlias maps a primary (canonical) path to one or more deprecated
|
|
|
|
|
// paths that should serve the same handler.
|
|
|
|
|
type DeprecatedPathAlias struct {
|
|
|
|
|
Method string
|
|
|
|
|
PrimaryPath string // canonical path (must already be registered)
|
|
|
|
|
DeprecatedPaths []string // old paths to alias
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// deprecatedPathInfoKey is the context key for deprecated URL path info.
|
|
|
|
|
type deprecatedPathInfoKey struct{}
|
|
|
|
|
|
|
|
|
|
// deprecatedPathInfo holds the deprecated and canonical paths for logging.
|
|
|
|
|
type deprecatedPathInfo struct {
|
|
|
|
|
deprecatedPath string
|
|
|
|
|
primaryPath string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LogDeprecatedPathAlias is a kithttp.RequestFunc (ServerBefore function)
|
|
|
|
|
// that checks if the request is using a deprecated URL path alias and, if so,
|
|
|
|
|
// elevates the log level to Warn and adds deprecation info to the request log.
|
|
|
|
|
// It must run after the LoggingContext is created (i.e. after SetRequestsContexts).
|
|
|
|
|
func LogDeprecatedPathAlias(ctx context.Context, _ *http.Request) context.Context {
|
|
|
|
|
if !platform_logging.TopicEnabled(platform_logging.DeprecatedFieldTopic) {
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
info, ok := ctx.Value(deprecatedPathInfoKey{}).(deprecatedPathInfo)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
logging.WithLevel(ctx, slog.LevelWarn)
|
|
|
|
|
logging.WithExtras(ctx,
|
|
|
|
|
"deprecated_path", info.deprecatedPath,
|
|
|
|
|
"deprecation_warning", fmt.Sprintf("API `%s` is deprecated, use `%s` instead", info.deprecatedPath, info.primaryPath),
|
|
|
|
|
)
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterDeprecatedPathAliases registers deprecated URL path aliases that point
|
|
|
|
|
// to the same handler as the canonical path, and wraps them in a handler that
|
|
|
|
|
// can log deprecation warnings.
|
|
|
|
|
func RegisterDeprecatedPathAliases(r *mux.Router, versions []string, registry *HandlerRegistry, aliases []DeprecatedPathAlias) {
|
|
|
|
|
allVersions := append(append([]string{}, versions...), "latest")
|
|
|
|
|
versionRegex := strings.Join(allVersions, "|")
|
|
|
|
|
for _, alias := range aliases {
|
|
|
|
|
handler := registry.handlers[handlerKey{alias.Method, alias.PrimaryPath}]
|
|
|
|
|
if handler == nil {
|
|
|
|
|
panic(fmt.Sprintf("deprecated alias: no handler registered for %s %s", alias.Method, alias.PrimaryPath))
|
|
|
|
|
}
|
|
|
|
|
for _, path := range alias.DeprecatedPaths {
|
|
|
|
|
// Replace the version placeholder in the deprecated path with a regex that matches all versions,
|
|
|
|
|
// so that the same handler can be used for all versions of the deprecated path.
|
|
|
|
|
pathForHandler := strings.Replace(path, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", versionRegex), 1)
|
|
|
|
|
info := deprecatedPathInfo{deprecatedPath: path, primaryPath: alias.PrimaryPath}
|
|
|
|
|
// Wrap the handler to inject deprecation info into the context for logging.
|
|
|
|
|
wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
ctx := context.WithValue(r.Context(), deprecatedPathInfoKey{}, info)
|
|
|
|
|
handler.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
|
})
|
|
|
|
|
nameAndVerb := getNameFromPathAndVerb(alias.Method, path, "")
|
|
|
|
|
r.Handle(pathForHandler, wrappedHandler).Name(nameAndVerb).Methods(alias.Method)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 15:05:02 +00:00
|
|
|
type CommonEndpointer[H any] struct {
|
2025-12-31 15:12:00 +00:00
|
|
|
EP Endpointer[H]
|
2026-02-05 15:29:53 +00:00
|
|
|
MakeDecoderFn func(iface any, requestBodyLimit int64) kithttp.DecodeRequestFunc
|
2025-12-31 15:12:00 +00:00
|
|
|
EncodeFn kithttp.EncodeResponseFunc
|
|
|
|
|
Opts []kithttp.ServerOption
|
|
|
|
|
Router *mux.Router
|
|
|
|
|
Versions []string
|
|
|
|
|
|
|
|
|
|
// AuthMiddleware is a pre-built authentication middleware.
|
2025-12-08 15:05:02 +00:00
|
|
|
AuthMiddleware endpoint.Middleware
|
2025-02-18 17:09:43 +00:00
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
// CustomMiddleware are middlewares that run before authentication.
|
2025-02-18 17:09:43 +00:00
|
|
|
CustomMiddleware []endpoint.Middleware
|
2025-12-31 15:12:00 +00:00
|
|
|
// CustomMiddlewareAfterAuth are middlewares that run after authentication.
|
2025-09-16 16:26:00 +00:00
|
|
|
CustomMiddlewareAfterAuth []endpoint.Middleware
|
2025-02-18 17:09:43 +00:00
|
|
|
|
2026-02-26 04:20:35 +00:00
|
|
|
// HandlerRegistry, if set, records handlers by method+path for deprecated
|
|
|
|
|
// path alias lookup. The pointer is shared across shallow copies (created
|
|
|
|
|
// by builder methods like WithAltPaths) so all registrations land in the
|
|
|
|
|
// same map.
|
|
|
|
|
HandlerRegistry *HandlerRegistry
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
startingAtVersion string
|
|
|
|
|
endingAtVersion string
|
|
|
|
|
alternativePaths []string
|
|
|
|
|
usePathPrefix bool
|
2026-02-05 15:29:53 +00:00
|
|
|
|
|
|
|
|
// The limit of the request body size in bytes, if set to -1 there is no limit.
|
|
|
|
|
requestBodySizeLimit int64
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 15:05:02 +00:00
|
|
|
type Endpointer[H any] interface {
|
2025-12-31 15:12:00 +00:00
|
|
|
CallHandlerFunc(f H, ctx context.Context, request any, svc any) (platform_http.Errorer, error)
|
|
|
|
|
Service() any
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) POST(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "POST")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) GET(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "GET")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) PUT(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "PUT")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) PATCH(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "PATCH")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) DELETE(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "DELETE")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) HEAD(path string, f H, v interface{}) {
|
|
|
|
|
e.handleEndpoint(path, f, v, "HEAD")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) handleEndpoint(path string, f H, v interface{}, verb string) {
|
|
|
|
|
endpoint := e.makeEndpoint(f, v)
|
|
|
|
|
e.HandleHTTPHandler(path, endpoint, verb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) makeEndpoint(f H, v interface{}) http.Handler {
|
|
|
|
|
next := func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
|
|
|
return e.EP.CallHandlerFunc(f, ctx, request, e.EP.Service())
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 16:26:00 +00:00
|
|
|
// Apply "after auth" middleware (in reverse order so that the first wraps
|
|
|
|
|
// the second wraps the third etc.)
|
|
|
|
|
endp := next
|
|
|
|
|
if len(e.CustomMiddlewareAfterAuth) > 0 {
|
|
|
|
|
for i := len(e.CustomMiddlewareAfterAuth) - 1; i >= 0; i-- {
|
|
|
|
|
mw := e.CustomMiddlewareAfterAuth[i]
|
|
|
|
|
endp = mw(endp)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-08 15:05:02 +00:00
|
|
|
if e.AuthMiddleware == nil {
|
|
|
|
|
// This panic catches potential security issues during development.
|
|
|
|
|
panic("AuthMiddleware must be set on CommonEndpointer")
|
|
|
|
|
}
|
|
|
|
|
endp = e.AuthMiddleware(endp)
|
2025-09-16 16:26:00 +00:00
|
|
|
|
|
|
|
|
// Apply "before auth" middleware (in reverse order so that the first wraps
|
|
|
|
|
// the second wraps the third etc.)
|
2025-02-18 17:09:43 +00:00
|
|
|
for i := len(e.CustomMiddleware) - 1; i >= 0; i-- {
|
|
|
|
|
mw := e.CustomMiddleware[i]
|
|
|
|
|
endp = mw(endp)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:29:53 +00:00
|
|
|
// Default to MaxRequestBodySize if no limit is set, this ensures no endpointers are forgot
|
|
|
|
|
// -1 = no limit, so don't default to anything if that is set, which can only be set with the appropriate SKIP method.
|
|
|
|
|
if e.requestBodySizeLimit != -1 && (e.requestBodySizeLimit == 0 || e.requestBodySizeLimit < platform_http.MaxRequestBodySize) {
|
|
|
|
|
// If no value is configured set default, or if the set endpoint value is less than global default use default.
|
|
|
|
|
e.requestBodySizeLimit = platform_http.MaxRequestBodySize
|
|
|
|
|
}
|
|
|
|
|
return newServer(endp, e.MakeDecoderFn(v, e.requestBodySizeLimit), e.EncodeFn, e.Opts)
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newServer(e endpoint.Endpoint, decodeFn kithttp.DecodeRequestFunc, encodeFn kithttp.EncodeResponseFunc,
|
2025-03-20 14:09:57 +00:00
|
|
|
opts []kithttp.ServerOption,
|
|
|
|
|
) http.Handler {
|
2025-02-18 17:09:43 +00:00
|
|
|
// TODO: some handlers don't have authz checks, and because the SkipAuth call is done only in the
|
|
|
|
|
// endpoint handler, any middleware that raises errors before the handler is reached will end up
|
|
|
|
|
// returning authz check missing instead of the more relevant error. Should be addressed as part
|
|
|
|
|
// of #4406.
|
|
|
|
|
e = authzcheck.NewMiddleware().AuthzCheck()(e)
|
|
|
|
|
return kithttp.NewServer(e, decodeFn, encodeFn, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) StartingAtVersion(version string) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.startingAtVersion = version
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) EndingAtVersion(version string) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.endingAtVersion = version
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) WithAltPaths(paths ...string) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.alternativePaths = paths
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) WithCustomMiddleware(mws ...endpoint.Middleware) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.CustomMiddleware = mws
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 18:03:50 +00:00
|
|
|
func (e *CommonEndpointer[H]) AppendCustomMiddleware(mws ...endpoint.Middleware) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.CustomMiddleware = append(ae.CustomMiddleware, mws...)
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 16:26:00 +00:00
|
|
|
func (e *CommonEndpointer[H]) WithCustomMiddlewareAfterAuth(mws ...endpoint.Middleware) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.CustomMiddlewareAfterAuth = mws
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
func (e *CommonEndpointer[H]) UsePathPrefix() *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.usePathPrefix = true
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:29:53 +00:00
|
|
|
func (e *CommonEndpointer[H]) WithRequestBodySizeLimit(limit int64) *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
if limit > 0 {
|
|
|
|
|
// Only set it when the limit is more than 0
|
|
|
|
|
ae.requestBodySizeLimit = limit
|
|
|
|
|
}
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) SkipRequestBodySizeLimit() *CommonEndpointer[H] {
|
|
|
|
|
ae := *e
|
|
|
|
|
ae.requestBodySizeLimit = -1
|
|
|
|
|
return &ae
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
// PathHandler registers a handler for the verb and path. The pathHandler is
|
|
|
|
|
// a function that receives the actual path to which it will be mounted, and
|
|
|
|
|
// returns the actual http.Handler that will handle this endpoint. This is for
|
|
|
|
|
// when the handler needs to know on which path it was called.
|
|
|
|
|
func (e *CommonEndpointer[H]) PathHandler(verb, path string, pathHandler func(path string) http.Handler) {
|
|
|
|
|
e.HandlePathHandler(path, pathHandler, verb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) HandleHTTPHandler(path string, h http.Handler, verb string) {
|
|
|
|
|
self := func(_ string) http.Handler { return h }
|
|
|
|
|
e.HandlePathHandler(path, self, verb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pathReplacer = strings.NewReplacer(
|
|
|
|
|
"/", "_",
|
|
|
|
|
"{", "_",
|
|
|
|
|
"}", "_",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func getNameFromPathAndVerb(verb, path, startAt string) string {
|
|
|
|
|
prefix := strings.ToLower(verb) + "_"
|
|
|
|
|
if startAt != "" {
|
|
|
|
|
prefix += pathReplacer.Replace(startAt) + "_"
|
|
|
|
|
}
|
|
|
|
|
return prefix + pathReplacer.Replace(strings.TrimPrefix(strings.TrimRight(path, "/"), "/api/_version_/fleet/"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *CommonEndpointer[H]) HandlePathHandler(path string, pathHandler func(path string) http.Handler, verb string) {
|
|
|
|
|
versions := e.Versions
|
|
|
|
|
if e.startingAtVersion != "" {
|
|
|
|
|
startIndex := -1
|
|
|
|
|
for i, version := range versions {
|
|
|
|
|
if version == e.startingAtVersion {
|
|
|
|
|
startIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if startIndex == -1 {
|
|
|
|
|
panic("StartAtVersion is not part of the valid versions")
|
|
|
|
|
}
|
|
|
|
|
versions = versions[startIndex:]
|
|
|
|
|
}
|
|
|
|
|
if e.endingAtVersion != "" {
|
|
|
|
|
endIndex := -1
|
|
|
|
|
for i, version := range versions {
|
|
|
|
|
if version == e.endingAtVersion {
|
|
|
|
|
endIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if endIndex == -1 {
|
|
|
|
|
panic("EndAtVersion is not part of the valid versions")
|
|
|
|
|
}
|
|
|
|
|
versions = versions[:endIndex+1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if a version doesn't have a deprecation version, or the ending version is the latest one, then it's part of the
|
|
|
|
|
// latest
|
|
|
|
|
if e.endingAtVersion == "" || e.endingAtVersion == e.Versions[len(e.Versions)-1] {
|
|
|
|
|
versions = append(versions, "latest")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
versionedPath := strings.Replace(path, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", strings.Join(versions, "|")), 1)
|
|
|
|
|
nameAndVerb := getNameFromPathAndVerb(verb, path, e.startingAtVersion)
|
2026-02-26 04:20:35 +00:00
|
|
|
handler := pathHandler(versionedPath)
|
2025-02-18 17:09:43 +00:00
|
|
|
if e.usePathPrefix {
|
2026-02-26 04:20:35 +00:00
|
|
|
e.Router.PathPrefix(versionedPath).Handler(handler).Name(nameAndVerb).Methods(verb)
|
2025-02-18 17:09:43 +00:00
|
|
|
} else {
|
2026-02-26 04:20:35 +00:00
|
|
|
e.Router.Handle(versionedPath, handler).Name(nameAndVerb).Methods(verb)
|
|
|
|
|
}
|
|
|
|
|
if e.HandlerRegistry != nil {
|
|
|
|
|
e.HandlerRegistry.handlers[handlerKey{verb, path}] = handler
|
2025-02-18 17:09:43 +00:00
|
|
|
}
|
|
|
|
|
for _, alias := range e.alternativePaths {
|
|
|
|
|
nameAndVerb := getNameFromPathAndVerb(verb, alias, e.startingAtVersion)
|
|
|
|
|
versionedPath := strings.Replace(alias, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", strings.Join(versions, "|")), 1)
|
|
|
|
|
if e.usePathPrefix {
|
|
|
|
|
e.Router.PathPrefix(versionedPath).Handler(pathHandler(versionedPath)).Name(nameAndVerb).Methods(verb)
|
|
|
|
|
} else {
|
|
|
|
|
e.Router.Handle(versionedPath, pathHandler(versionedPath)).Name(nameAndVerb).Methods(verb)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func EncodeCommonResponse(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
w http.ResponseWriter,
|
|
|
|
|
response interface{},
|
|
|
|
|
jsonMarshal func(w http.ResponseWriter, response interface{}) error,
|
2025-12-31 15:12:00 +00:00
|
|
|
domainErrorEncoder DomainErrorEncoder,
|
2025-02-18 17:09:43 +00:00
|
|
|
) error {
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// Infer alias rules from `renameto` struct tags on the response type.
|
|
|
|
|
aliasRules := ExtractAliasRules(response)
|
2026-04-02 20:56:31 +00:00
|
|
|
if br, ok := response.(beforeRenderer); ok {
|
|
|
|
|
br.BeforeRender(ctx, w)
|
|
|
|
|
}
|
2025-07-07 18:13:46 +00:00
|
|
|
if cs, ok := response.(cookieSetter); ok {
|
|
|
|
|
cs.SetCookies(ctx, w)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
// The has to happen first, if an error happens we'll redirect to an error
|
|
|
|
|
// page and the error will be logged
|
|
|
|
|
if page, ok := response.(htmlPage); ok {
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
Add CSP to fleet(currently disabled - needs frontend work) (#41395)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40538
This is the initial iteration of CSP functionality, currently gated
behind FLEET_SERVER_ENABLE_CSP. If disabled, no CSP is served. Nonces
are still injected into pages however a dummy nonce is used and has no
effect.
With this setting turned on things break and will be addressed by mainly
frontend changes in https://github.com/fleetdm/fleet/issues/41577
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
---------
Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
2026-03-12 22:06:54 +00:00
|
|
|
// This will not return an error if disabled
|
|
|
|
|
_, _ = WriteBrowserSecurityHeaders(w, false, false)
|
2025-02-18 17:09:43 +00:00
|
|
|
if coder, ok := page.Error().(kithttp.StatusCoder); ok {
|
|
|
|
|
w.WriteHeader(coder.StatusCode())
|
|
|
|
|
}
|
|
|
|
|
_, err := io.WriteString(w, page.Html())
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 15:12:00 +00:00
|
|
|
if e, ok := response.(platform_http.Errorer); ok && e.Error() != nil {
|
|
|
|
|
EncodeError(ctx, e.Error(), w, domainErrorEncoder)
|
2025-02-18 17:09:43 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if render, ok := response.(renderHijacker); ok {
|
|
|
|
|
render.HijackRender(ctx, w)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if e, ok := response.(statuser); ok {
|
|
|
|
|
w.WriteHeader(e.Status())
|
|
|
|
|
if e.Status() == http.StatusNoContent {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// If alias rules are configured, buffer the JSON output so we can
|
|
|
|
|
// duplicate keys (old→new) for forwards compatibility before writing
|
|
|
|
|
// to the response.
|
|
|
|
|
if len(aliasRules) > 0 {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
bufWriter := &bufferedResponseWriter{ResponseWriter: w, buf: &buf}
|
|
|
|
|
if err := jsonMarshal(bufWriter, response); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
transformed := DuplicateJSONKeys(buf.Bytes(), aliasRules)
|
|
|
|
|
_, err := w.Write(transformed)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 17:09:43 +00:00
|
|
|
return jsonMarshal(w, response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// statuser allows response types to implement a custom
|
|
|
|
|
// http success status - default is 200 OK
|
|
|
|
|
type statuser interface {
|
|
|
|
|
Status() int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loads a html page
|
|
|
|
|
type htmlPage interface {
|
|
|
|
|
Html() string
|
|
|
|
|
Error() error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// renderHijacker can be implemented by response values to take control of
|
|
|
|
|
// their own rendering.
|
|
|
|
|
type renderHijacker interface {
|
|
|
|
|
HijackRender(ctx context.Context, w http.ResponseWriter)
|
|
|
|
|
}
|
2025-07-07 18:13:46 +00:00
|
|
|
|
|
|
|
|
// cookieSetter can be implemented by response values to set cookies on the response.
|
|
|
|
|
type cookieSetter interface {
|
|
|
|
|
SetCookies(ctx context.Context, w http.ResponseWriter)
|
|
|
|
|
}
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
|
2026-04-02 20:56:31 +00:00
|
|
|
// beforeRenderer can be implemented by response values that need to hook into the
|
|
|
|
|
// raw rendering process, with access to the ResponseWriter before any response is
|
|
|
|
|
// written, while continuing with the normal rendering process after the call.
|
|
|
|
|
// It can be used to set headers, for example, and since the processing happens before
|
|
|
|
|
// any Errorer check, it can also be used to fail the request by storing an error on
|
|
|
|
|
// the Errorer. It should not set the status code of the response, as the standard
|
|
|
|
|
// approach of implementing the statuser interface should be used for that.
|
|
|
|
|
//
|
|
|
|
|
// Unlike renderHijacker and the htmlPage interfaces, this interface does not stop
|
|
|
|
|
// processing, and while it behaves similarly to cookieSetter, it is more generally-named
|
|
|
|
|
// and does not have the specific connotation of setting cookies.
|
|
|
|
|
type beforeRenderer interface {
|
|
|
|
|
BeforeRender(ctx context.Context, w http.ResponseWriter)
|
|
|
|
|
}
|
|
|
|
|
|
Deprecate "team" and "query" API params (#39873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** For #39344
# Details
This PR builds on the previous PR
(https://github.com/fleetdm/fleet/pull/39847) which added `renameto`
tags to certain API parameters to mark them as deprecated. How this is
used:
### In requests
* When decoding requests, log a warning if a `json` or `query` param is
used that has a `renameto` tag, e.g. if a `team_id` param is sent but
the related struct has `renameto:"fleet_id"` in it.
* If the `renamedto` version (e.g. `fleet_id`) is sent in the request,
rewrite it to the deprecated name so that it can be unmarshalled into
the struct
* If both versions are sent (e.g. `team_id` AND `fleet_id`), throw an
error and quit
* URLs with deprecated terms have new aliases using `WithAltPaths` --
warning on using old URLSs a TODO that will be handled in a subsequent
PR.
### In responses
* Output _both_ the deprecated and new names for fields that have
`renameto` tags, so that we don't break existing workflows expecting the
old keys. Uses a shared `DuplicateJSONKeys` to do the duplication.
* Most API responses are handled in `EncodeCommonResponse`. Exceptions
are activities, failing policy webhooks and the streaming "list hosts"
endpoints which call the function directly.
### In fleetctl
* Similar to requests, log warnings when deprecated keys are used and
rewrite the new keys internally so that they can be unmarshalled.
* For `fleetctl get` and `fleetctl generate-gitops`, _only_ output the
new names
* The set of keys to replace is hardcoded in `fleetctl` rather than
being dynamically generated as it is for API endpoints. Given the
mixture of typed and untyped data and the level of nesting, dynamic map
generation was very fragile and error-prone.
### Performance considerations
* The biggest performance hit is the addition of the JSON key rewriter
to the request pipeline. The rewriter buffers the entire request into
memory before eventually passing it to the decoder than unmarshals the
data into structs. I tried implementing this as a true streaming
rewriter but encountered issues where the request would hang if the
downstream reader (the decoder) encountered any errors. It's possible we
could implement this in a streaming fashion if we replace our [current
request
decoder](https://github.com/fleetdm/fleet/blob/da43bf8371695382c4af0972d5da456c6b94bdaf/server/service/endpoint_utils.go#L108)
with the v2 version, which is a bigger change requiring more thoughtful
discussion in the engineering team. As it stands, memory usage for
requests with deprecated fields will double while the request is being
decoded.
* The "alias rules" used to determine the old and new key names are
cached per struct type and for most endpoints are generated on server
start, so no performance impact is expected.
* Some `fleetctl` commands may have an extra unmarshal/marshal step but
as these are user-initiated and not performed in tight loops, the impact
should be minimal.
### TODO
* Log deprecation warnings when old URLs like "/fleet/teams" are used
* Update API fields that the front-end uses to avoid deprecation
warnings
* Update `fleetctl apply` to accept/return `kind: fleet` rather than
`kind: team`
* Find/update any fleet server config vars with old language
* Update all error messages that use old language
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [X] 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.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [X] QA'd all new/changed functionality manually
* Clicking around the front-end, no broken pages due to request
ingestion errors or bad responses
* Looking in network tab to verify that responses have both the old and
new keys
* Running `fleetctl generate-gitops` and verifying that the output looks
correct and can be ingested by `fleetctl gitops`
* Running `fleetctl get` and `fleetctl apply`
---------
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
2026-02-19 19:53:32 +00:00
|
|
|
// bufferedResponseWriter wraps an http.ResponseWriter but redirects Write
|
|
|
|
|
// calls to a bytes.Buffer, allowing the output to be captured and
|
|
|
|
|
// transformed before being sent to the real writer. It implements
|
|
|
|
|
// http.ResponseWriter so it can be passed to jsonMarshal functions.
|
|
|
|
|
type bufferedResponseWriter struct {
|
|
|
|
|
http.ResponseWriter
|
|
|
|
|
buf *bytes.Buffer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *bufferedResponseWriter) Write(data []byte) (int, error) {
|
|
|
|
|
return b.buf.Write(data)
|
|
|
|
|
}
|