fleet/server/service/device_client.go

121 lines
3.2 KiB
Go
Raw Normal View History

package service
import (
"bytes"
"errors"
"fmt"
"net/http"
"github.com/fleetdm/fleet/v4/server/fleet"
)
// Device client is used consume the `device/...` endpoints and meant to be used by Fleet Desktop
type DeviceClient struct {
*baseClient
}
// NewDeviceClient instantiates a new client to perform requests against device
// endpoints
func NewDeviceClient(addr string, insecureSkipVerify bool, rootCA string) (*DeviceClient, error) {
capabilities := fleet.CapabilityMap{}
baseClient, err := newBaseClient(addr, insecureSkipVerify, rootCA, "", capabilities)
if err != nil {
return nil, err
}
return &DeviceClient{
baseClient: baseClient,
}, nil
}
func (dc *DeviceClient) request(verb string, path string, query string, responseDest interface{}) error {
var bodyBytes []byte
request, err := http.NewRequest(
verb,
dc.url(path, query).String(),
bytes.NewBuffer(bodyBytes),
)
if err != nil {
return err
}
add headers denoting capabilities between fleet server / desktop / orbit (#7833) This adds a new mechanism to allow us to handle compatibility issues between Orbit, Fleet Server and Fleet Desktop. The general idea is to _always_ send a custom header of the form: ``` fleet-capabilities-header = "X-Fleet-Capabilities:" capabilities capabilities = capability * (,) capability = string ``` Both from the server to the clients (Orbit, Fleet Desktop) and vice-versa. For an example, see: https://github.com/fleetdm/fleet/commit/8c0bbdd291f54e03e19766bcdfead0fb8067f60c Also, the following applies: - Backwards compat: if the header is not present, assume that orbit/fleet doesn't have the capability - The current capabilities endpoint will be removed ### Motivation This solution is trying to solve the following problems: - We have three independent processes communicating with each other (Fleet Desktop, Orbit and Fleet Server). Each process can be updated independently, and therefore we need a way for each process to know what features are supported by its peers. - We originally implemented a dedicated API endpoint in the server that returned a list of the capabilities (or "features") enabled, we found this, and any other server-only solution (like API versioning) to be insufficient because: - There are cases in which the server also needs to know which features are supported by its clients - Clients needed to poll for changes to detect if the capabilities supported by the server change, by sending the capabilities on each request we have a much cleaner way to handling different responses. - We are also introducing an unauthenticated endpoint to get the server features, this gives us flexibility if we need to implement different authentication mechanisms, and was one of the pitfalls of the first implementation. Related to https://github.com/fleetdm/fleet/issues/7929
2022-09-26 10:53:53 +00:00
dc.setClientCapabilitiesHeader(request)
response, err := dc.http.Do(request)
if err != nil {
return fmt.Errorf("%s %s: %w", verb, path, err)
}
defer response.Body.Close()
return dc.parseResponse(verb, path, response, responseDest)
}
// TransparencyURL returns an URL that the server will use to redirect to the
// transparency URL configured by the user
func (dc *DeviceClient) TransparencyURL(token string) string {
return dc.baseClient.url("/api/latest/fleet/device/"+token+"/transparency", "").String()
}
// DeviceURL returns the public device URL for the given token
func (dc *DeviceClient) DeviceURL(token string) string {
return dc.baseClient.url("/device/"+token, "").String()
}
// CheckToken checks if a token is valid by making an authenticated request to
// the server
func (dc *DeviceClient) CheckToken(token string) error {
_, err := dc.NumberOfFailingPolicies(token)
return err
}
// Ping sends a ping to the server using the device/ping endpoint
func (dc *DeviceClient) Ping() error {
verb, path := "HEAD", "/api/fleet/device/ping"
err := dc.request(verb, path, "", nil)
if err == nil || errors.Is(err, notFoundErr{}) {
// notFound is ok, it means an old server without the ping endpoint +
// capabilities header
return nil
}
return err
}
func (dc *DeviceClient) getListDevicePolicies(token string) ([]*fleet.HostPolicy, error) {
verb, path := "GET", "/api/latest/fleet/device/"+token+"/policies"
var responseBody listDevicePoliciesResponse
err := dc.request(verb, path, "", &responseBody)
return responseBody.Policies, err
}
func (dc *DeviceClient) getMinDesktopPayload(token string) (fleetDesktopResponse, error) {
verb, path := "GET", "/api/latest/fleet/device/"+token+"/desktop"
var r fleetDesktopResponse
err := dc.request(verb, path, "", &r)
return r, err
}
func (dc *DeviceClient) NumberOfFailingPolicies(token string) (uint, error) {
r, err := dc.getMinDesktopPayload(token)
if err == nil {
return uintValueOrZero(r.FailingPolicies), nil
}
if errors.Is(err, notFoundErr{}) {
policies, err := dc.getListDevicePolicies(token)
if err != nil {
return 0, err
}
var failingPolicies uint
for _, policy := range policies {
if policy.Response != "pass" {
failingPolicies++
}
}
return failingPolicies, nil
}
return 0, err
}