2022-09-23 19:00:23 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2022-09-26 14:44:09 +00:00
|
|
|
"errors"
|
2022-09-23 19:00:23 +00:00
|
|
|
"fmt"
|
2022-10-03 20:28:19 +00:00
|
|
|
"io/fs"
|
|
|
|
"io/ioutil"
|
2022-09-23 19:00:23 +00:00
|
|
|
"net/http"
|
2022-10-03 20:28:19 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-01-26 21:51:24 +00:00
|
|
|
"runtime"
|
2022-10-03 20:28:19 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
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
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
2023-01-26 21:51:24 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/pkg/retry"
|
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
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2022-10-03 20:28:19 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2022-09-23 19:00:23 +00:00
|
|
|
)
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// OrbitClient exposes the Orbit API to communicate with the Fleet server.
|
2022-09-23 19:00:23 +00:00
|
|
|
type OrbitClient struct {
|
|
|
|
*baseClient
|
2022-10-03 20:28:19 +00:00
|
|
|
nodeKeyFilePath string
|
|
|
|
enrollSecret string
|
|
|
|
uuid string
|
2023-02-28 17:55:04 +00:00
|
|
|
serial string
|
2022-10-03 20:28:19 +00:00
|
|
|
|
|
|
|
enrolledMu sync.Mutex
|
|
|
|
enrolled bool
|
|
|
|
|
|
|
|
lastRecordedErrMu sync.Mutex
|
|
|
|
lastRecordedErr error
|
2022-10-28 17:27:21 +00:00
|
|
|
|
|
|
|
// TestNodeKey is used for testing only.
|
|
|
|
TestNodeKey string
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (oc *OrbitClient) request(verb string, path string, params interface{}, resp interface{}) error {
|
|
|
|
var bodyBytes []byte
|
|
|
|
var err error
|
|
|
|
if params != nil {
|
|
|
|
bodyBytes, err = json.Marshal(params)
|
|
|
|
if err != nil {
|
2022-10-03 20:28:19 +00:00
|
|
|
return fmt.Errorf("making request json marshalling : %w", err)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
request, err := http.NewRequest(
|
|
|
|
verb,
|
|
|
|
oc.url(path, "").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
|
|
|
oc.setClientCapabilitiesHeader(request)
|
2022-09-23 19:00:23 +00:00
|
|
|
response, err := oc.http.Do(request)
|
|
|
|
if err != nil {
|
2022-10-03 20:28:19 +00:00
|
|
|
oc.setLastRecordedError(err)
|
2022-09-23 19:00:23 +00:00
|
|
|
return fmt.Errorf("%s %s: %w", verb, path, err)
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
if err := oc.parseResponse(verb, path, response, resp); err != nil {
|
|
|
|
oc.setLastRecordedError(err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// NewOrbitClient creates a new OrbitClient.
|
|
|
|
//
|
|
|
|
// rootDir is the Orbit's root directory, where the Orbit node key is loaded-from/stored.
|
|
|
|
// addr is the address of the Fleet server.
|
|
|
|
// uuid is the UUID of the OrbitClient instance.
|
2023-02-28 17:55:04 +00:00
|
|
|
func NewOrbitClient(rootDir string, addr string, rootCA string, insecureSkipVerify bool, enrollSecret, uuid, serialNum string) (*OrbitClient, error) {
|
2022-10-03 20:28:19 +00:00
|
|
|
orbitCapabilities := fleet.CapabilityMap{}
|
|
|
|
bc, err := newBaseClient(addr, insecureSkipVerify, rootCA, "", orbitCapabilities)
|
2022-09-23 19:00:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
nodeKeyFilePath := filepath.Join(rootDir, constant.OrbitNodeKeyFileName)
|
2022-09-23 19:00:23 +00:00
|
|
|
return &OrbitClient{
|
2022-10-03 20:28:19 +00:00
|
|
|
nodeKeyFilePath: nodeKeyFilePath,
|
|
|
|
baseClient: bc,
|
|
|
|
enrollSecret: enrollSecret,
|
|
|
|
uuid: uuid,
|
2023-02-28 17:55:04 +00:00
|
|
|
serial: serialNum,
|
2022-10-03 20:28:19 +00:00
|
|
|
enrolled: false,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetConfig returns the Orbit config fetched from Fleet server for this instance of OrbitClient.
|
2023-01-25 20:03:40 +00:00
|
|
|
func (oc *OrbitClient) GetConfig() (*fleet.OrbitConfig, error) {
|
2022-10-03 20:28:19 +00:00
|
|
|
verb, path := "POST", "/api/fleet/orbit/config"
|
|
|
|
var resp orbitGetConfigResponse
|
|
|
|
if err := oc.authenticatedRequest(verb, path, &orbitGetConfigRequest{}, &resp); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-01-25 20:03:40 +00:00
|
|
|
return &fleet.OrbitConfig{
|
2023-01-17 18:19:48 +00:00
|
|
|
Flags: resp.Flags,
|
|
|
|
Extensions: resp.Extensions,
|
|
|
|
Notifications: resp.Notifications,
|
2023-02-10 20:03:43 +00:00
|
|
|
NudgeConfig: resp.NudgeConfig,
|
2022-09-23 19:00:23 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-10-10 20:15:35 +00:00
|
|
|
// SetOrUpdateDeviceToken sends a request to the server to set or update the
|
|
|
|
// device token with the given value.
|
|
|
|
func (oc *OrbitClient) SetOrUpdateDeviceToken(deviceAuthToken string) error {
|
|
|
|
verb, path := "POST", "/api/fleet/orbit/device_token"
|
|
|
|
params := setOrUpdateDeviceTokenRequest{
|
|
|
|
DeviceAuthToken: deviceAuthToken,
|
|
|
|
}
|
|
|
|
var resp setOrUpdateDeviceTokenResponse
|
|
|
|
if err := oc.authenticatedRequest(verb, path, ¶ms, &resp); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ping sends a ping request to the orbit/ping endpoint.
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) Ping() error {
|
|
|
|
verb, path := "HEAD", "/api/fleet/orbit/ping"
|
|
|
|
err := oc.request(verb, path, nil, nil)
|
|
|
|
if err == nil || errors.Is(err, notFoundErr{}) {
|
|
|
|
// notFound is ok, it means an old server without the capabilities header
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (oc *OrbitClient) enroll() (string, error) {
|
2022-09-23 19:00:23 +00:00
|
|
|
verb, path := "POST", "/api/fleet/orbit/enroll"
|
2023-02-28 17:55:04 +00:00
|
|
|
params := EnrollOrbitRequest{EnrollSecret: oc.enrollSecret, HardwareUUID: oc.uuid, HardwareSerial: oc.serial}
|
2022-10-28 17:27:21 +00:00
|
|
|
var resp EnrollOrbitResponse
|
2022-09-23 19:00:23 +00:00
|
|
|
err := oc.request(verb, path, params, &resp)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return resp.OrbitNodeKey, nil
|
|
|
|
}
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
// enrollLock helps protect the enrolling process in case mutliple OrbitClients
|
|
|
|
// want to re-enroll at the same time.
|
|
|
|
var enrollLock sync.Mutex
|
|
|
|
|
|
|
|
// getNodeKeyOrEnroll attempts to read the orbit node key if the file exists on disk
|
|
|
|
// otherwise it enrolls the host with Fleet and saves the node key to disk
|
|
|
|
func (oc *OrbitClient) getNodeKeyOrEnroll() (string, error) {
|
2022-10-28 17:27:21 +00:00
|
|
|
if oc.TestNodeKey != "" {
|
|
|
|
return oc.TestNodeKey, nil
|
|
|
|
}
|
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
enrollLock.Lock()
|
|
|
|
defer enrollLock.Unlock()
|
|
|
|
|
|
|
|
orbitNodeKey, err := ioutil.ReadFile(oc.nodeKeyFilePath)
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
return string(orbitNodeKey), nil
|
|
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
|
|
// OK, if there's no orbit node key, proceed to enroll.
|
|
|
|
default:
|
|
|
|
return "", fmt.Errorf("read orbit node key file: %w", err)
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
2022-10-03 20:28:19 +00:00
|
|
|
var (
|
|
|
|
orbitNodeKey_ string
|
|
|
|
endpointDoesNotExist bool
|
|
|
|
)
|
|
|
|
if err := retry.Do(
|
|
|
|
func() error {
|
|
|
|
var err error
|
|
|
|
orbitNodeKey_, err = oc.enrollAndWriteNodeKeyFile()
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
return nil
|
|
|
|
case errors.Is(err, notFoundErr{}):
|
|
|
|
// Do not retry if the endpoint does not exist.
|
|
|
|
endpointDoesNotExist = true
|
|
|
|
return nil
|
|
|
|
default:
|
|
|
|
log.Info().Err(err).Msg("enroll failed, retrying")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
},
|
|
|
|
retry.WithInterval(constant.OrbitEnrollRetrySleep),
|
|
|
|
retry.WithMaxAttempts(constant.OrbitEnrollMaxRetries),
|
|
|
|
); err != nil {
|
|
|
|
return "", fmt.Errorf("orbit node key enroll failed, attempts=%d", constant.OrbitEnrollMaxRetries)
|
|
|
|
}
|
|
|
|
if endpointDoesNotExist {
|
|
|
|
return "", errors.New("enroll endpoint does not exist")
|
|
|
|
}
|
|
|
|
return orbitNodeKey_, nil
|
|
|
|
}
|
2022-09-23 19:00:23 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) enrollAndWriteNodeKeyFile() (string, error) {
|
|
|
|
orbitNodeKey, err := oc.enroll()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("enroll request: %w", err)
|
|
|
|
}
|
2023-01-26 21:51:24 +00:00
|
|
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
|
|
|
// creating the secret file with empty content
|
|
|
|
if err := os.WriteFile(oc.nodeKeyFilePath, nil, constant.DefaultFileMode); err != nil {
|
|
|
|
return "", fmt.Errorf("create orbit node key file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// restricting file access
|
|
|
|
if err := platform.ChmodRestrictFile(oc.nodeKeyFilePath); err != nil {
|
|
|
|
return "", fmt.Errorf("apply ACLs: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// writing raw key material to the acl-ready secret file
|
2022-10-03 20:28:19 +00:00
|
|
|
if err := os.WriteFile(oc.nodeKeyFilePath, []byte(orbitNodeKey), constant.DefaultFileMode); err != nil {
|
|
|
|
return "", fmt.Errorf("write orbit node key file: %w", err)
|
|
|
|
}
|
2023-01-26 21:51:24 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
return orbitNodeKey, nil
|
2022-09-23 19:00:23 +00:00
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) authenticatedRequest(verb string, path string, params interface{}, resp interface{}) error {
|
|
|
|
nodeKey, err := oc.getNodeKeyOrEnroll()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
s := params.(setOrbitNodeKeyer)
|
|
|
|
s.setOrbitNodeKey(nodeKey)
|
|
|
|
|
2022-11-29 16:54:36 +00:00
|
|
|
err = oc.request(verb, path, params, resp)
|
|
|
|
switch {
|
2022-10-03 20:28:19 +00:00
|
|
|
case err == nil:
|
|
|
|
oc.setEnrolled(true)
|
2022-09-26 14:44:09 +00:00
|
|
|
return nil
|
2022-10-03 20:28:19 +00:00
|
|
|
case errors.Is(err, ErrUnauthenticated):
|
|
|
|
if err := os.Remove(oc.nodeKeyFilePath); err != nil {
|
|
|
|
log.Info().Err(err).Msg("remove orbit node key")
|
|
|
|
}
|
|
|
|
oc.setEnrolled(false)
|
|
|
|
return err
|
|
|
|
default:
|
|
|
|
return err
|
2022-09-26 14:44:09 +00:00
|
|
|
}
|
2022-10-03 20:28:19 +00:00
|
|
|
}
|
2022-09-26 14:44:09 +00:00
|
|
|
|
2022-10-03 20:28:19 +00:00
|
|
|
func (oc *OrbitClient) Enrolled() bool {
|
|
|
|
oc.enrolledMu.Lock()
|
|
|
|
defer oc.enrolledMu.Unlock()
|
|
|
|
|
|
|
|
return oc.enrolled
|
|
|
|
}
|
|
|
|
|
|
|
|
func (oc *OrbitClient) setEnrolled(v bool) {
|
|
|
|
oc.enrolledMu.Lock()
|
|
|
|
defer oc.enrolledMu.Unlock()
|
|
|
|
|
|
|
|
oc.enrolled = v
|
|
|
|
}
|
|
|
|
|
|
|
|
func (oc *OrbitClient) LastRecordedError() error {
|
|
|
|
oc.lastRecordedErrMu.Lock()
|
|
|
|
defer oc.lastRecordedErrMu.Unlock()
|
|
|
|
|
|
|
|
return oc.lastRecordedErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (oc *OrbitClient) setLastRecordedError(err error) {
|
|
|
|
oc.lastRecordedErrMu.Lock()
|
|
|
|
defer oc.lastRecordedErrMu.Unlock()
|
|
|
|
|
|
|
|
oc.lastRecordedErr = fmt.Errorf("%s: %w", time.Now().UTC().Format("2006-01-02T15:04:05Z"), err)
|
2022-09-26 14:44:09 +00:00
|
|
|
}
|