mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
For #32040. --- Backend changes to unblock the development of the orbit and frontend changes. New GET and PUT APIs for setting/getting software for Linux Setup Experience: ``` curl -k -X GET -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/setup_experience/linux/software?team_id=8&per_page=3000 curl -k -X PUT -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/setup_experience/linux/software -d '{"team_id":8,"software_title_ids":[3000, 3001, 3007]}' ``` New setup_experience/init API called by orbit to trigger the Linux setup experience on the device: ``` curl -v -k -X POST -H "Content-Type: application/json" "https://localhost:8080/api/fleet/orbit/setup_experience/init" -d '{"orbit_node_key": "ynYEtFsvv9xZ7rX619UE8of1I28H+GCj"}' ``` Get status API to call on "My device": ``` curl -v -k -X POST "https://localhost:8080/api/latest/fleet/device/7d940b6e-130a-493b-b58a-2b6e9f9f8bfc/setup_experience/status" ``` --- - [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. ## 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 ## New Fleet configuration settings - [X] Verified that the setting is exported via `fleetctl generate-gitops` - [X] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [X] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Added Linux support for Setup Experience alongside macOS. - Introduced platform-specific admin APIs to configure and retrieve Setup Experience software (macOS/Linux). - Added device API to report Setup Experience status and an Orbit API to initialize Setup Experience on non-macOS devices. - Setup Experience now gates policy queries on Linux until setup is complete. - New activity log entry when Setup Experience software is edited (includes platform and team). - Documentation - Updated audit logs reference to include the new “edited setup experience software” event. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
241 lines
6.4 KiB
Go
241 lines
6.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/capabilities"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
|
|
eu "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
|
"github.com/go-kit/kit/endpoint"
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
"github.com/go-kit/log"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc {
|
|
return eu.MakeDecoder(iface, jsonDecode, parseCustomTags, isBodyDecoder, decodeBody)
|
|
}
|
|
|
|
// A value that implements bodyDecoder takes control of decoding the request body.
|
|
type bodyDecoder interface {
|
|
DecodeBody(ctx context.Context, r io.Reader, u url.Values, c []*x509.Certificate) error
|
|
}
|
|
|
|
func decodeBody(ctx context.Context, r *http.Request, v reflect.Value, body io.Reader) error {
|
|
bd := v.Interface().(bodyDecoder)
|
|
var certs []*x509.Certificate
|
|
if (r.TLS != nil) && (r.TLS.PeerCertificates != nil) {
|
|
certs = r.TLS.PeerCertificates
|
|
}
|
|
|
|
if err := bd.DecodeBody(ctx, body, r.URL.Query(), certs); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseCustomTags(urlTagValue string, r *http.Request, field reflect.Value) (bool, error) {
|
|
switch urlTagValue {
|
|
case "list_options":
|
|
opts, err := listOptionsFromRequest(r)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
field.Set(reflect.ValueOf(opts))
|
|
return true, nil
|
|
|
|
case "user_options":
|
|
opts, err := userListOptionsFromRequest(r)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
field.Set(reflect.ValueOf(opts))
|
|
return true, nil
|
|
|
|
case "host_options":
|
|
opts, err := hostListOptionsFromRequest(r)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
field.Set(reflect.ValueOf(opts))
|
|
return true, nil
|
|
|
|
case "carve_options":
|
|
opts, err := carveListOptionsFromRequest(r)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
field.Set(reflect.ValueOf(opts))
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func jsonDecode(body io.Reader, req any) error {
|
|
return json.NewDecoder(body).Decode(req)
|
|
}
|
|
|
|
func isBodyDecoder(v reflect.Value) bool {
|
|
_, ok := v.Interface().(bodyDecoder)
|
|
return ok
|
|
}
|
|
|
|
// Compile-time check to ensure that endpointer implements Endpointer.
|
|
var _ eu.Endpointer[eu.HandlerFunc] = &endpointer{}
|
|
|
|
type endpointer struct {
|
|
svc fleet.Service
|
|
}
|
|
|
|
func (e *endpointer) CallHandlerFunc(f eu.HandlerFunc, ctx context.Context, request interface{},
|
|
svc interface{},
|
|
) (fleet.Errorer, error) {
|
|
return f(ctx, request, svc.(fleet.Service))
|
|
}
|
|
|
|
func (e *endpointer) Service() interface{} {
|
|
return e.svc
|
|
}
|
|
|
|
func newUserAuthenticatedEndpointer(svc fleet.Service, opts []kithttp.ServerOption, r *mux.Router,
|
|
versions ...string,
|
|
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
|
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
|
EP: &endpointer{
|
|
svc: svc,
|
|
},
|
|
MakeDecoderFn: makeDecoder,
|
|
EncodeFn: encodeResponse,
|
|
Opts: opts,
|
|
AuthFunc: auth.AuthenticatedUser,
|
|
FleetService: svc,
|
|
Router: r,
|
|
Versions: versions,
|
|
}
|
|
}
|
|
|
|
func newNoAuthEndpointer(svc fleet.Service, opts []kithttp.ServerOption, r *mux.Router,
|
|
versions ...string,
|
|
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
|
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
|
EP: &endpointer{
|
|
svc: svc,
|
|
},
|
|
MakeDecoderFn: makeDecoder,
|
|
EncodeFn: encodeResponse,
|
|
Opts: opts,
|
|
AuthFunc: auth.UnauthenticatedRequest,
|
|
FleetService: svc,
|
|
Router: r,
|
|
Versions: versions,
|
|
}
|
|
}
|
|
|
|
func badRequest(msg string) error {
|
|
return &fleet.BadRequestError{Message: msg}
|
|
}
|
|
|
|
func badRequestf(format string, a ...any) error {
|
|
return &fleet.BadRequestError{
|
|
Message: fmt.Sprintf(format, a...),
|
|
}
|
|
}
|
|
|
|
func newDeviceAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router,
|
|
versions ...string,
|
|
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
|
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
return authenticatedDevice(svc, logger, next)
|
|
}
|
|
|
|
// Inject the fleet.CapabilitiesHeader header to the response for device endpoints
|
|
opts = append(opts, capabilitiesResponseFunc(fleet.GetServerDeviceCapabilities()))
|
|
// Add the capabilities reported by the device to the request context
|
|
opts = append(opts, capabilitiesContextFunc())
|
|
|
|
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
|
EP: &endpointer{
|
|
svc: svc,
|
|
},
|
|
MakeDecoderFn: makeDecoder,
|
|
EncodeFn: encodeResponse,
|
|
Opts: opts,
|
|
AuthFunc: authFunc,
|
|
FleetService: svc,
|
|
Router: r,
|
|
Versions: versions,
|
|
}
|
|
}
|
|
|
|
func newHostAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router,
|
|
versions ...string,
|
|
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
|
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
return authenticatedHost(svc, logger, next)
|
|
}
|
|
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
|
EP: &endpointer{
|
|
svc: svc,
|
|
},
|
|
MakeDecoderFn: makeDecoder,
|
|
EncodeFn: encodeResponse,
|
|
Opts: opts,
|
|
AuthFunc: authFunc,
|
|
FleetService: svc,
|
|
Router: r,
|
|
Versions: versions,
|
|
}
|
|
}
|
|
|
|
func newOrbitAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router,
|
|
versions ...string,
|
|
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
|
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
return authenticatedOrbitHost(svc, logger, next)
|
|
}
|
|
|
|
// Inject the fleet.Capabilities header to the response for Orbit hosts
|
|
opts = append(opts, capabilitiesResponseFunc(fleet.GetServerOrbitCapabilities()))
|
|
// Add the capabilities reported by Orbit to the request context
|
|
opts = append(opts, capabilitiesContextFunc())
|
|
|
|
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
|
EP: &endpointer{
|
|
svc: svc,
|
|
},
|
|
MakeDecoderFn: makeDecoder,
|
|
EncodeFn: encodeResponse,
|
|
Opts: opts,
|
|
AuthFunc: authFunc,
|
|
FleetService: svc,
|
|
Router: r,
|
|
Versions: versions,
|
|
}
|
|
}
|
|
|
|
func capabilitiesResponseFunc(capabilities fleet.CapabilityMap) kithttp.ServerOption {
|
|
return kithttp.ServerAfter(func(ctx context.Context, w http.ResponseWriter) context.Context {
|
|
writeCapabilitiesHeader(w, capabilities)
|
|
return ctx
|
|
})
|
|
}
|
|
|
|
func capabilitiesContextFunc() kithttp.ServerOption {
|
|
return kithttp.ServerBefore(capabilities.NewContext)
|
|
}
|
|
|
|
func writeCapabilitiesHeader(w http.ResponseWriter, capabilities fleet.CapabilityMap) {
|
|
if len(capabilities) == 0 {
|
|
return
|
|
}
|
|
|
|
w.Header().Set(fleet.CapabilitiesHeader, capabilities.String())
|
|
}
|