fleet/server/service/orbit.go
Lucas Manuel Rodriguez 3757aace08
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129 

Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.

Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
  "message": "Bad request",
  "errors": [
    {
      "name": "base",
      "reason": "Expected JSON Body"
    }
  ],
  "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
  "message": "Bad request",
  "errors": [
    {
      "name": "base",
      "reason": "json decoder error"
    }
  ],
  "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
  "message": "Authentication required",
  "errors": [
    {
      "name": "base",
      "reason": "Authentication required"
    }
  ],
  "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
  "message": "Authorization header required",
  "errors": [
    {
      "name": "base",
      "reason": "Authorization header required"
    }
  ],
  "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
  "message": "Permission Denied",
  "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 13:44:06 -03:00

289 lines
8.8 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
)
type setOrbitNodeKeyer interface {
setOrbitNodeKey(nodeKey string)
}
type orbitError struct {
message string
}
type EnrollOrbitRequest struct {
EnrollSecret string `json:"enroll_secret"`
HardwareUUID string `json:"hardware_uuid"`
HardwareSerial string `json:"hardware_serial"`
}
type EnrollOrbitResponse struct {
OrbitNodeKey string `json:"orbit_node_key,omitempty"`
Err error `json:"error,omitempty"`
}
type orbitGetConfigRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
}
func (r *orbitGetConfigRequest) setOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *orbitGetConfigRequest) orbitHostNodeKey() string {
return r.OrbitNodeKey
}
type orbitGetConfigResponse struct {
fleet.OrbitConfig
Err error `json:"error,omitempty"`
}
func (r orbitGetConfigResponse) error() error { return r.Err }
func (e orbitError) Error() string {
return e.message
}
func (r EnrollOrbitResponse) error() error { return r.Err }
// hijackRender so we can add a header with the server capabilities in the
// response, allowing Orbit to know what features are available without the
// need to enroll.
func (r EnrollOrbitResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
writeCapabilitiesHeader(w, fleet.ServerOrbitCapabilities)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(r); err != nil {
encodeError(ctx, newOsqueryError(fmt.Sprintf("orbit enroll failed: %s", err)), w)
}
}
func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*EnrollOrbitRequest)
nodeKey, err := svc.EnrollOrbit(ctx, req.HardwareUUID, req.HardwareSerial, req.EnrollSecret)
if err != nil {
return EnrollOrbitResponse{Err: err}, nil
}
return EnrollOrbitResponse{OrbitNodeKey: nodeKey}, nil
}
func (svc *Service) AuthenticateOrbitHost(ctx context.Context, orbitNodeKey string) (*fleet.Host, bool, error) {
svc.authz.SkipAuthorization(ctx)
if orbitNodeKey == "" {
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing orbit node key"))
}
host, err := svc.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
switch {
case err == nil:
// OK
case fleet.IsNotFound(err):
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid orbit node key"))
default:
return nil, false, ctxerr.Wrap(ctx, err, "authentication error orbit")
}
return host, svc.debugEnabledForHost(ctx, host.ID), nil
}
// EnrollOrbit returns an orbit nodeKey on successful enroll
func (svc *Service) EnrollOrbit(ctx context.Context, hardwareUUID, hardwareSerial, enrollSecret string) (string, error) {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
logging.WithExtras(ctx, "hardware_uuid", hardwareUUID, "hardware_serial", hardwareSerial)
secret, err := svc.ds.VerifyEnrollSecret(ctx, enrollSecret)
if err != nil {
return "", orbitError{message: err.Error()}
}
orbitNodeKey, err := server.GenerateRandomText(svc.config.Osquery.NodeKeySize)
if err != nil {
return "", orbitError{message: "failed to generate orbit node key: " + err.Error()}
}
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return "", orbitError{message: "app config load failed: " + err.Error()}
}
_, err = svc.ds.EnrollOrbit(ctx, appConfig.MDM.EnabledAndConfigured, hardwareUUID, hardwareSerial, orbitNodeKey, secret.TeamID)
if err != nil {
return "", orbitError{message: "failed to enroll " + err.Error()}
}
return orbitNodeKey, nil
}
func getOrbitConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
cfg, err := svc.GetOrbitConfig(ctx)
if err != nil {
return orbitGetConfigResponse{Err: err}, nil
}
return orbitGetConfigResponse{OrbitConfig: cfg}, nil
}
func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, error) {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
var notifs fleet.OrbitConfigNotifications
host, ok := hostctx.FromContext(ctx)
if !ok {
return fleet.OrbitConfig{Notifications: notifs}, orbitError{message: "internal error: missing host from request context"}
}
// set the host's orbit notifications
if host.IsOsqueryEnrolled() && host.MDMInfo.IsPendingDEPFleetEnrollment() {
notifs.RenewEnrollmentProfile = true
}
// team ID is not nil, get team specific flags and options
if host.TeamID != nil {
teamAgentOptions, err := svc.ds.TeamAgentOptions(ctx, *host.TeamID)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
var opts fleet.AgentOptions
if teamAgentOptions != nil && len(*teamAgentOptions) > 0 {
if err := json.Unmarshal(*teamAgentOptions, &opts); err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
}
mdmConfig, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
var nudgeConfig *fleet.NudgeConfig
if mdmConfig != nil &&
mdmConfig.MacOSUpdates.Deadline != "" &&
mdmConfig.MacOSUpdates.MinimumVersion != "" {
nudgeConfig, err = fleet.NewNudgeConfig(mdmConfig.MacOSUpdates)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
}
return fleet.OrbitConfig{
Flags: opts.CommandLineStartUpFlags,
Extensions: opts.Extensions,
Notifications: notifs,
NudgeConfig: nudgeConfig,
}, nil
}
// team ID is nil, get global flags and options
config, err := svc.ds.AppConfig(ctx)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
var opts fleet.AgentOptions
if config.AgentOptions != nil {
if err := json.Unmarshal(*config.AgentOptions, &opts); err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
}
var nudgeConfig *fleet.NudgeConfig
if config.MDM.MacOSUpdates.Deadline != "" &&
config.MDM.MacOSUpdates.MinimumVersion != "" {
nudgeConfig, err = fleet.NewNudgeConfig(config.MDM.MacOSUpdates)
if err != nil {
return fleet.OrbitConfig{Notifications: notifs}, err
}
}
return fleet.OrbitConfig{
Flags: opts.CommandLineStartUpFlags,
Extensions: opts.Extensions,
Notifications: notifs,
NudgeConfig: nudgeConfig,
}, nil
}
/////////////////////////////////////////////////////////////////////////////////
// Ping orbit endpoint
/////////////////////////////////////////////////////////////////////////////////
type orbitPingRequest struct{}
type orbitPingResponse struct{}
func (r orbitPingResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
writeCapabilitiesHeader(w, fleet.ServerOrbitCapabilities)
}
func (r orbitPingResponse) error() error { return nil }
// NOTE: we're intentionally not reading the capabilities header in this
// endpoint as is unauthenticated and we don't want to trust whatever comes in
// there.
func orbitPingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
svc.DisableAuthForPing(ctx)
return orbitPingResponse{}, nil
}
/////////////////////////////////////////////////////////////////////////////////
// SetOrUpdateDeviceToken endpoint
/////////////////////////////////////////////////////////////////////////////////
type setOrUpdateDeviceTokenRequest struct {
OrbitNodeKey string `json:"orbit_node_key"`
DeviceAuthToken string `json:"device_auth_token"`
}
func (r *setOrUpdateDeviceTokenRequest) setOrbitNodeKey(nodeKey string) {
r.OrbitNodeKey = nodeKey
}
func (r *setOrUpdateDeviceTokenRequest) orbitHostNodeKey() string {
return r.OrbitNodeKey
}
type setOrUpdateDeviceTokenResponse struct {
Err error `json:"error,omitempty"`
}
func (r setOrUpdateDeviceTokenResponse) error() error { return r.Err }
func setOrUpdateDeviceTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*setOrUpdateDeviceTokenRequest)
if err := svc.SetOrUpdateDeviceAuthToken(ctx, req.DeviceAuthToken); err != nil {
return setOrUpdateDeviceTokenResponse{Err: err}, nil
}
return setOrUpdateDeviceTokenResponse{}, nil
}
func (svc *Service) SetOrUpdateDeviceAuthToken(ctx context.Context, deviceAuthToken string) error {
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
host, ok := hostctx.FromContext(ctx)
if !ok {
return newOsqueryError("internal error: missing host from request context")
}
if err := svc.ds.SetOrUpdateDeviceAuthToken(ctx, host.ID, deviceAuthToken); err != nil {
return newOsqueryError(fmt.Sprintf("internal error: failed to set or update device auth token: %e", err))
}
return nil
}