device_token endpoint improvements (#15849)

Fixed badly formatted error messages in /api/fleet/orbit/device_token
endpoint and others.
In /api/fleet/orbit/device_token:
- Added token validation -- empty token not allowed
- Replaced 500 error with 409 when token conflicts with another host

#15832 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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] Added/updated tests
- [x] Manual QA
This commit is contained in:
Victor Lyuboslavsky
2023-12-28 14:20:36 -06:00
committed by GitHub
parent 02dea5ac9d
commit ebf1650671
9 changed files with 85 additions and 9 deletions
+4
View File
@@ -0,0 +1,4 @@
Fixed badly formatted error message in /api/fleet/orbit/device_token endpoint and others.
In /api/fleet/orbit/device_token:
- Added token validation -- empty token not allowed
- Replaced 500 error with 409 when token conflicts with another host
+5 -5
View File
@@ -16,7 +16,7 @@ func rSign(pkgPath, cert string) error {
defer os.Remove(pemPath)
err := os.WriteFile(pemPath, []byte(cert), 0o600)
if err != nil {
return fmt.Errorf("writing cert data: %e", err)
return fmt.Errorf("writing cert data: %s", err)
}
return retry.Do(func() error {
@@ -41,7 +41,7 @@ func rNotarizeStaple(pkg, apiKeyID, apiKeyIssuer, apiKeyContent string) error {
path, err := writeAPIKeys(apiKeyIssuer, apiKeyID, apiKeyContent)
defer os.Remove(path)
if err != nil {
return fmt.Errorf("writing API keys: %e", err)
return fmt.Errorf("writing API keys: %s", err)
}
return retry.Do(func() error {
@@ -66,19 +66,19 @@ func rNotarizeStaple(pkg, apiKeyID, apiKeyIssuer, apiKeyContent string) error {
func writeAPIKeys(issuer, id, content string) (string, error) {
homedir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("finding home dir: %e", err)
return "", fmt.Errorf("finding home dir: %s", err)
}
// The underliying tools (rcodesign and Transporter) expect to find a
// certificate key in this path.
path := filepath.Join(homedir, ".appstoreconnect", "private_keys")
if err = secure.MkdirAll(path, 0o600); err != nil {
return "", fmt.Errorf("finding home dir: %e", err)
return "", fmt.Errorf("finding home dir: %s", err)
}
keyPath := filepath.Join(path, fmt.Sprintf("AuthKey_%s.p8", id))
if err = os.WriteFile(keyPath, []byte(content), 0o600); err != nil {
return "", fmt.Errorf("writing api key contents: %e", err)
return "", fmt.Errorf("writing api key contents: %s", err)
}
return keyPath, nil
+1 -1
View File
@@ -121,7 +121,7 @@ func (n *NudgeConfigFetcher) setTargetsAndHashes() error {
// we don't want to keep nudge as a target if we failed to update the
// cached hashes in the runner.
if err := n.opt.UpdateRunner.StoreLocalHash("nudge"); err != nil {
log.Debug().Msgf("removing nudge from target options, error updating local hashes: %e", err)
log.Debug().Msgf("removing nudge from target options, error updating local hashes: %s", err)
n.opt.UpdateRunner.RemoveRunnerOptTarget("nudge")
n.opt.UpdateRunner.updater.RemoveTargetInfo("nudge")
return err
+1 -1
View File
@@ -54,7 +54,7 @@ func (s *SwiftDialogDownloader) GetConfig() (*fleet.OrbitConfig, error) {
// we don't want to keep swiftDialog as a target if we failed to update the
// cached hashes in the runner.
if err := s.UpdateRunner.StoreLocalHash("swiftDialog"); err != nil {
log.Debug().Msgf("removing swiftDialog from target options, error updating local hashes: %e", err)
log.Debug().Msgf("removing swiftDialog from target options, error updating local hashes: %s", err)
s.UpdateRunner.RemoveRunnerOptTarget("swiftDialog")
s.UpdateRunner.updater.RemoveTargetInfo("swiftDialog")
return cfg, err
+3
View File
@@ -2266,6 +2266,9 @@ func (ds *Datastore) SetOrUpdateDeviceAuthToken(ctx context.Context, hostID uint
`
_, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, authToken)
if err != nil {
if isDuplicate(err) {
return fleet.ConflictError{Message: "auth token conflicts with another host"}
}
return ctxerr.Wrap(ctx, err, "upsert host's device auth token")
}
return nil
+15
View File
@@ -543,3 +543,18 @@ const (
RunScriptAlreadyRunningErrMsg = "A script is already running on this host. Please wait about 1 minute to let it finish."
RunScriptHostTimeoutErrMsg = "Fleet hasnt heard from the host in over 1 minute. Fleet doesnt know if the script ran because the host went offline."
)
// ConflictError is used to indicate a conflict, such as a UUID conflict in the DB.
type ConflictError struct {
Message string
}
// Error implements the error interface for the ConflictError.
func (e ConflictError) Error() string {
return e.Message
}
// StatusCode implements the kithttp.StatusCoder interface.
func (e ConflictError) StatusCode() int {
return http.StatusConflict
}
+1 -1
View File
@@ -242,7 +242,7 @@ type APNSDeliveryError struct {
}
func (e *APNSDeliveryError) Error() string {
return fmt.Sprintf("APNS delivery failed with: %e, for UUIDs: %v", e.Err, e.FailedUUIDs)
return fmt.Sprintf("APNS delivery failed with: %s, for UUIDs: %v", e.Err, e.FailedUUIDs)
}
func (e *APNSDeliveryError) Unwrap() error { return e.Err }
+46
View File
@@ -9335,3 +9335,49 @@ func (s *integrationTestSuite) TestHostHealth() {
assert.Empty(t, resp.HostHealth.FailingPolicies)
assert.Nil(t, resp.HostHealth.TeamID)
}
func (s *integrationTestSuite) TestHostDeviceToken() {
t := s.T()
type response struct {
Err string `json:"error"`
}
orbitHost := createOrbitEnrolledHost(t, "windows", "device_token", s.ds)
// Write empty token
body := setOrUpdateDeviceTokenRequest{
OrbitNodeKey: *orbitHost.OrbitNodeKey,
DeviceAuthToken: "",
}
s.DoJSON("POST", "/api/fleet/orbit/device_token", body, http.StatusBadRequest, &response{})
// Write bad node key
body = setOrUpdateDeviceTokenRequest{
OrbitNodeKey: "",
DeviceAuthToken: "token",
}
s.DoJSON("POST", "/api/fleet/orbit/device_token", body, http.StatusUnauthorized, &response{})
// Write a good token.
body = setOrUpdateDeviceTokenRequest{
OrbitNodeKey: *orbitHost.OrbitNodeKey,
DeviceAuthToken: "token",
}
s.DoJSON("POST", "/api/fleet/orbit/device_token", body, http.StatusOK, &response{})
// Try to write the token again for a different host.
// First write a valid token.
orbitHost2 := createOrbitEnrolledHost(t, "darwin", "device_token2", s.ds)
body = setOrUpdateDeviceTokenRequest{
OrbitNodeKey: *orbitHost2.OrbitNodeKey,
DeviceAuthToken: "token2",
}
s.DoJSON("POST", "/api/fleet/orbit/device_token", body, http.StatusOK, &response{})
// Now write a duplicate token, which will result in a conflict with the first host.
body = setOrUpdateDeviceTokenRequest{
OrbitNodeKey: *orbitHost2.OrbitNodeKey,
DeviceAuthToken: "token",
}
s.DoJSON("POST", "/api/fleet/orbit/device_token", body, http.StatusConflict, &response{})
}
+9 -1
View File
@@ -3,6 +3,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
@@ -423,13 +424,20 @@ func (svc *Service) SetOrUpdateDeviceAuthToken(ctx context.Context, deviceAuthTo
// this is not a user-authenticated endpoint
svc.authz.SkipAuthorization(ctx)
if len(deviceAuthToken) == 0 {
return badRequest("device auth token cannot be empty")
}
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))
if errors.As(err, &fleet.ConflictError{}) {
return err
}
return newOsqueryError(fmt.Sprintf("internal error: failed to set or update device auth token: %s", err))
}
return nil