Files
fleet/server/service/endpoint_middleware.go
Lucas Manuel Rodriguez 9100a1265d Move carve requests to server/fleet/ (#45785)
Resolves #36087 (one of several PRs)

## Testing

- [x] QA'd all new/changed functionality manually.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Reorganized internal API request/response types for carve operations
to centralize type definitions and improve code maintainability.

* **Tests**
* Updated carve operation tests to align with refactored code structure.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45785?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-20 09:12:38 -03:00

283 lines
8.5 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/certserial"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/osqueryauth"
"github.com/fleetdm/fleet/v4/server/fleet"
middleware_log "github.com/fleetdm/fleet/v4/server/service/middleware/log"
kithttp "github.com/go-kit/kit/transport/http"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/go-kit/kit/endpoint"
)
// extractCertSerialFromHeader extracts certificate serial from X-Client-Cert-Serial
// header (set by load balancer during mTLS) for iOS/iPadOS device authentication.
func extractCertSerialFromHeader(ctx context.Context, r *http.Request) context.Context {
serialStr := r.Header.Get("X-Client-Cert-Serial")
if serialStr == "" {
return ctx
}
serial, err := strconv.ParseUint(serialStr, 10, 64)
if err != nil {
// Force cert auth on parse error instead of falling back to token auth.
return certserial.NewContext(ctx, 0)
}
return certserial.NewContext(ctx, serial)
}
func logJSON(ctx context.Context, logger *slog.Logger, v any, key string) {
jsonV, err := json.Marshal(v)
if err != nil {
logger.DebugContext(ctx, "error marshaling for debug", "key", key, "err", err)
return
}
logger.DebugContext(ctx, "debug JSON", key, string(jsonV))
}
// instrumentHostLogger adds host ID, IP information, and extras to the context logger.
func instrumentHostLogger(ctx context.Context, hostID uint, extras ...interface{}) {
remoteAddr, _ := ctx.Value(kithttp.ContextKeyRequestRemoteAddr).(string)
xForwardedFor, _ := ctx.Value(kithttp.ContextKeyRequestXForwardedFor).(string)
logging.WithExtras(
logging.WithNoUser(ctx),
append(extras,
"host_id", hostID,
"ip_addr", remoteAddr,
"x_for_ip_addr", xForwardedFor,
)...,
)
}
// authenticatedDevice checks the validity of the device auth token
// provided in the request, and attaches the corresponding host to the
// context for the request.
func authenticatedDevice(svc fleet.Service, logger *slog.Logger, next endpoint.Endpoint) endpoint.Endpoint {
authDeviceFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
identifier, err := getDeviceAuthToken(request)
if err != nil {
return nil, err
}
var host *fleet.Host
var debug bool
var authnMethod authz_ctx.AuthenticationMethod
if certSerial, ok := certserial.FromContext(ctx); ok {
// Header presence signals cert auth intent, even if serial is invalid.
host, debug, err = svc.AuthenticateDeviceByCertificate(ctx, certSerial, identifier)
authnMethod = authz_ctx.AuthnDeviceCertificate
} else {
// Try token auth first (hot path for Fleet Desktop).
host, debug, err = svc.AuthenticateDevice(ctx, identifier)
if err == nil {
authnMethod = authz_ctx.AuthnDeviceToken
} else {
// Fallback to UUID auth for iOS/iPadOS self-service via URL.
// The identifier (from {token}) is treated as the device UUID.
host, debug, err = svc.AuthenticateIDeviceByURL(ctx, identifier)
authnMethod = authz_ctx.AuthnDeviceURL
}
}
if err != nil {
logging.WithErr(ctx, err)
return nil, err
}
hlogger := logger.With("host_id", host.ID)
if debug {
logJSON(ctx, hlogger, request, "request")
}
ctx = hostctx.NewContext(ctx, host)
// Register host as error context provider for ctxerr enrichment
hostProvider := &hostctx.HostAttributeProvider{Host: host}
ctx = ctxerr.AddErrorContextProvider(ctx, hostProvider)
instrumentHostLogger(ctx, host.ID)
if ac, ok := authz_ctx.FromContext(ctx); ok {
ac.SetAuthnMethod(authnMethod)
}
resp, err := next(ctx, request)
if err != nil {
return nil, err
}
if debug {
logJSON(ctx, hlogger, resp, "response")
}
return resp, nil
}
return middleware_log.Logged(authDeviceFunc)
}
func getDeviceAuthToken(r interface{}) (string, error) {
if dat, ok := r.(interface{ deviceAuthToken() string }); ok {
return dat.deviceAuthToken(), nil
}
return "", fleet.NewAuthRequiredError("request type does not implement deviceAuthToken method. This is likely a Fleet programmer error.")
}
// authenticatedHost wraps an endpoint, checks the validity of the node_key
// provided in the request, and attaches the corresponding osquery host to the
// context for the request.
//
// If the HTTP pre-auth middleware (osqueryHeaderPreAuth) has already
// authenticated the request via the Authorization: NodeKey <token> header,
// the hostctx and related ctx setup is already in place and this middleware
// becomes a passthrough.
func authenticatedHost(svc fleet.Service, logger *slog.Logger, next endpoint.Endpoint) endpoint.Endpoint {
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
// HTTP pre-auth already authenticated the request and populated
// hostctx.
if osqueryauth.IsPreAuthed(ctx) {
host, ok := hostctx.FromContext(ctx)
if !ok {
return nil, ctxerr.New(ctx, "osquery pre-auth marker set without host in ctx")
}
instrumentHostLogger(ctx, host.ID)
if ac, ok := authz_ctx.FromContext(ctx); ok {
ac.SetAuthnMethod(authz_ctx.AuthnHostToken)
}
debug := osqueryauth.IsDebug(ctx)
var hlogger *slog.Logger
if debug {
hlogger = logger.With("host_id", host.ID)
logJSON(ctx, hlogger, request, "request")
}
resp, err := next(ctx, request)
if err != nil {
return nil, err
}
if debug {
logJSON(ctx, hlogger, resp, "response")
}
return resp, nil
}
nodeKey, err := getNodeKey(request)
if err != nil {
return nil, err
}
host, debug, err := svc.AuthenticateHost(ctx, nodeKey)
if err != nil {
logging.WithErr(ctx, err)
return nil, err
}
hlogger := logger.With("host_id", host.ID)
if debug {
logJSON(ctx, hlogger, request, "request")
}
ctx = hostctx.NewContext(ctx, host)
// Register host as error context provider for ctxerr enrichment
hostProvider := &hostctx.HostAttributeProvider{Host: host}
ctx = ctxerr.AddErrorContextProvider(ctx, hostProvider)
instrumentHostLogger(ctx, host.ID)
if ac, ok := authz_ctx.FromContext(ctx); ok {
ac.SetAuthnMethod(authz_ctx.AuthnHostToken)
}
resp, err := next(ctx, request)
if err != nil {
return nil, err
}
if debug {
logJSON(ctx, hlogger, resp, "response")
}
return resp, nil
}
return middleware_log.Logged(authHostFunc)
}
func authenticatedOrbitHost(
svc fleet.Service,
logger *slog.Logger,
next endpoint.Endpoint,
orbitNodeKeyGetter func(context.Context, interface{}) (string, error),
) endpoint.Endpoint {
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
nodeKey, err := orbitNodeKeyGetter(ctx, request)
if err != nil {
return nil, err
}
host, debug, err := svc.AuthenticateOrbitHost(ctx, nodeKey)
if err != nil {
logging.WithErr(ctx, err)
return nil, err
}
hlogger := logger.With("host_id", host.ID)
if debug {
logJSON(ctx, hlogger, request, "request")
}
ctx = hostctx.NewContext(ctx, host)
// Register host as error context provider for ctxerr enrichment
hostProvider := &hostctx.HostAttributeProvider{Host: host}
ctx = ctxerr.AddErrorContextProvider(ctx, hostProvider)
instrumentHostLogger(ctx, host.ID)
if ac, ok := authz_ctx.FromContext(ctx); ok {
ac.SetAuthnMethod(authz_ctx.AuthnOrbitToken)
}
resp, err := next(ctx, request)
if err != nil {
return nil, err
}
if debug {
logJSON(ctx, hlogger, resp, "response")
}
return resp, nil
}
return middleware_log.Logged(authHostFunc)
}
func getOrbitNodeKey(ctx context.Context, r interface{}) (string, error) {
if onk, ok := r.(interface{ OrbitHostNodeKey() string }); ok {
return onk.OrbitHostNodeKey(), nil
}
return "", errors.New("error getting orbit node key")
}
func authHeaderValue(prefix string) func(ctx context.Context, r interface{}) (string, error) {
return func(ctx context.Context, r interface{}) (string, error) {
if authHeader, ok := ctx.Value(kithttp.ContextKeyRequestAuthorization).(string); ok {
return strings.TrimPrefix(authHeader, prefix), nil
}
return "", nil
}
}
func getNodeKey(r interface{}) (string, error) {
if hnk, ok := r.(interface{ HostNodeKey() string }); ok {
return hnk.HostNodeKey(), nil
}
if hnk, ok := r.(interface{ hostNodeKey() string }); ok {
return hnk.hostNodeKey(), nil
}
return "", newOsqueryError("request type does not implement hostNodeKey method. This is likely a Fleet programmer error.")
}