fleet/server/activity/internal/service/endpoint_utils.go
Magnus Jensen da43bf8371
Request body limits (#39080)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves
https://github.com/fleetdm/confidential/issues/13934

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually
2026-02-05 10:29:53 -05:00

153 lines
4.7 KiB
Go

package service
import (
"context"
"encoding/json"
"io"
"net/http"
"reflect"
"strconv"
"github.com/fleetdm/fleet/v4/server/activity/api"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
eu "github.com/fleetdm/fleet/v4/server/platform/endpointer"
platform_http "github.com/fleetdm/fleet/v4/server/platform/http"
"github.com/go-kit/kit/endpoint"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
)
const (
// defaultPerPage is used when per_page is not specified but page is specified.
defaultPerPage = 20
// unlimitedPerPage is used when neither page nor per_page is specified,
// effectively returning all results (legacy behavior for backwards compatibility).
unlimitedPerPage = 1000000
)
// encodeResponse encodes the response as JSON.
func encodeResponse(ctx context.Context, w http.ResponseWriter, response any) error {
return eu.EncodeCommonResponse(ctx, w, response,
func(w http.ResponseWriter, response any) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(response)
},
nil, // no domain-specific error encoder
)
}
// makeDecoder creates a decoder for the given request type.
func makeDecoder(iface any, requestBodySizeLimit int64) kithttp.DecodeRequestFunc {
return eu.MakeDecoder(iface, func(body io.Reader, req any) error {
return json.NewDecoder(body).Decode(req)
}, parseCustomTags, nil, nil, nil, requestBodySizeLimit)
}
// parseCustomTags handles custom URL tag values for activity requests.
func parseCustomTags(urlTagValue string, r *http.Request, field reflect.Value) (bool, error) {
if urlTagValue == "list_options" {
opts, err := listOptionsFromRequest(r)
if err != nil {
return false, err
}
field.Set(reflect.ValueOf(opts))
return true, nil
}
return false, nil
}
// listOptionsFromRequest parses list options from query parameters.
func listOptionsFromRequest(r *http.Request) (api.ListOptions, error) {
q := r.URL.Query()
var page int
if val := q.Get("page"); val != "" {
var err error
page, err = strconv.Atoi(val)
if err != nil {
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "non-int page value"})
}
if page < 0 {
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "negative page value"})
}
}
var perPage int
if val := q.Get("per_page"); val != "" {
var err error
perPage, err = strconv.Atoi(val)
if err != nil {
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "non-int per_page value"})
}
if perPage <= 0 {
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "invalid per_page value"})
}
}
orderKey := q.Get("order_key")
orderDirectionString := q.Get("order_direction")
if orderKey == "" && orderDirectionString != "" {
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "order_key must be specified with order_direction"})
}
var orderDirection api.OrderDirection
switch orderDirectionString {
case "desc":
orderDirection = api.OrderDescending
case "asc", "":
orderDirection = api.OrderAscending
default:
return api.ListOptions{}, ctxerr.Wrap(r.Context(), &platform_http.BadRequestError{Message: "unknown order_direction: " + orderDirectionString})
}
return api.ListOptions{
Page: uint(page), //nolint:gosec // dismiss G115
PerPage: uint(perPage), //nolint:gosec // dismiss G115
OrderKey: orderKey,
OrderDirection: orderDirection,
After: q.Get("after"),
ActivityType: q.Get("activity_type"),
StartCreatedAt: q.Get("start_created_at"),
EndCreatedAt: q.Get("end_created_at"),
MatchQuery: q.Get("query"),
}, nil
}
// handlerFunc is the handler function type for Activity service endpoints.
type handlerFunc func(ctx context.Context, request any, svc api.Service) platform_http.Errorer
// Compile-time check to ensure endpointer implements Endpointer.
var _ eu.Endpointer[handlerFunc] = &endpointer{}
type endpointer struct {
svc api.Service
}
func (e *endpointer) CallHandlerFunc(f handlerFunc, ctx context.Context,
request any,
svc any,
) (platform_http.Errorer, error) {
return f(ctx, request, svc.(api.Service)), nil
}
func (e *endpointer) Service() any {
return e.svc
}
func newUserAuthenticatedEndpointer(svc api.Service, authMiddleware endpoint.Middleware, opts []kithttp.ServerOption, r *mux.Router,
versions ...string,
) *eu.CommonEndpointer[handlerFunc] {
return &eu.CommonEndpointer[handlerFunc]{
EP: &endpointer{
svc: svc,
},
MakeDecoderFn: makeDecoder,
EncodeFn: encodeResponse,
Opts: opts,
AuthMiddleware: authMiddleware,
Router: r,
Versions: versions,
}
}