2022-07-18 17:22:49 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
2025-09-26 18:03:50 +00:00
|
|
|
"testing"
|
2022-07-18 17:22:49 +00:00
|
|
|
"time"
|
|
|
|
|
|
2024-10-31 19:24:42 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2022-07-18 17:22:49 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
2025-09-26 18:03:50 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/datastore/redis"
|
2022-07-18 17:22:49 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2022-12-26 21:32:39 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
2022-07-18 17:22:49 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
2024-10-31 19:24:42 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2022-07-18 17:22:49 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (s *integrationTestSuite) TestDeviceAuthenticatedEndpoints() {
|
|
|
|
|
t := s.T()
|
|
|
|
|
|
|
|
|
|
hosts := s.createHosts(t)
|
|
|
|
|
ac, err := s.ds.AppConfig(context.Background())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
ac.OrgInfo.OrgLogoURL = "http://example.com/logo"
|
2024-05-29 12:54:48 +00:00
|
|
|
ac.OrgInfo.ContactURL = "http://example.com/contact"
|
|
|
|
|
ac.Features.EnableSoftwareInventory = true
|
2022-07-18 17:22:49 +00:00
|
|
|
err = s.ds.SaveAppConfig(context.Background(), ac)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// create some mappings and MDM/Munki data
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, s.ds.ReplaceHostDeviceMapping(context.Background(), hosts[0].ID, []*fleet.HostDeviceMapping{
|
2023-12-21 17:21:39 +00:00
|
|
|
{HostID: hosts[0].ID, Email: "a@b.c", Source: fleet.DeviceMappingGoogleChromeProfiles},
|
|
|
|
|
{HostID: hosts[0].ID, Email: "b@b.c", Source: fleet.DeviceMappingGoogleChromeProfiles},
|
|
|
|
|
}, fleet.DeviceMappingGoogleChromeProfiles))
|
|
|
|
|
_, err = s.ds.SetOrUpdateCustomHostDeviceMapping(context.Background(), hosts[0].ID, "c@b.c", fleet.DeviceMappingCustomInstaller)
|
|
|
|
|
require.NoError(t, err)
|
2025-07-22 21:24:19 +00:00
|
|
|
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), hosts[0].ID, false, true, "url", false, "", "", false))
|
2022-08-29 18:40:16 +00:00
|
|
|
require.NoError(t, s.ds.SetOrUpdateMunkiInfo(context.Background(), hosts[0].ID, "1.3.0", nil, nil))
|
2022-07-18 17:22:49 +00:00
|
|
|
// create a battery for hosts[0]
|
|
|
|
|
require.NoError(t, s.ds.ReplaceHostBatteries(context.Background(), hosts[0].ID, []*fleet.HostBattery{
|
2024-10-02 21:43:19 +00:00
|
|
|
{HostID: hosts[0].ID, SerialNumber: "a", CycleCount: 1, Health: "Normal"},
|
2022-07-18 17:22:49 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// create an auth token for hosts[0]
|
|
|
|
|
token := "much_valid"
|
|
|
|
|
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
|
|
|
|
|
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, hosts[0].ID, token)
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// get host without token
|
|
|
|
|
res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/", nil, http.StatusNotFound)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
// get host with invalid token
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/no_such_token", nil, http.StatusUnauthorized)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
2023-02-01 17:47:52 +00:00
|
|
|
// set the mdm configured flag
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
appCfg, err := s.ds.AppConfig(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
appCfg.MDM.EnabledAndConfigured = true
|
|
|
|
|
err = s.ds.SaveAppConfig(ctx, appCfg)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
appCfg.MDM.EnabledAndConfigured = false
|
|
|
|
|
err = s.ds.SaveAppConfig(ctx, appCfg)
|
|
|
|
|
})
|
|
|
|
|
|
2022-07-18 17:22:49 +00:00
|
|
|
// get host with valid token
|
|
|
|
|
var getHostResp getDeviceHostResponse
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getHostResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Equal(t, hosts[0].ID, getHostResp.Host.ID)
|
|
|
|
|
require.False(t, getHostResp.Host.RefetchRequested)
|
|
|
|
|
require.Equal(t, "http://example.com/logo", getHostResp.OrgLogoURL)
|
2024-05-29 12:54:48 +00:00
|
|
|
require.Equal(t, "http://example.com/contact", getHostResp.OrgContactURL)
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Nil(t, getHostResp.Host.Policies)
|
|
|
|
|
require.NotNil(t, getHostResp.Host.Batteries)
|
2022-07-21 02:16:03 +00:00
|
|
|
require.Equal(t, &fleet.HostBattery{CycleCount: 1, Health: "Normal"}, (*getHostResp.Host.Batteries)[0])
|
2023-02-01 17:47:52 +00:00
|
|
|
require.True(t, getHostResp.GlobalConfig.MDM.EnabledAndConfigured)
|
2024-05-29 12:54:48 +00:00
|
|
|
require.True(t, getHostResp.GlobalConfig.Features.EnableSoftwareInventory)
|
2022-07-18 17:22:49 +00:00
|
|
|
hostDevResp := getHostResp.Host
|
|
|
|
|
|
2023-05-23 18:01:04 +00:00
|
|
|
// make request for same host on the host details API endpoint,
|
|
|
|
|
// responses should match, except for policies and DEP assignment
|
2022-07-18 17:22:49 +00:00
|
|
|
getHostResp = getDeviceHostResponse{}
|
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hosts[0].ID), nil, http.StatusOK, &getHostResp)
|
|
|
|
|
getHostResp.Host.Policies = nil
|
2023-05-23 18:01:04 +00:00
|
|
|
getHostResp.Host.DEPAssignedToFleet = ptr.Bool(false)
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Equal(t, hostDevResp, getHostResp.Host)
|
|
|
|
|
|
|
|
|
|
// request a refetch for that valid host
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/refetch", nil, http.StatusOK)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
// host should have that flag turned to true
|
|
|
|
|
getHostResp = getDeviceHostResponse{}
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getHostResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.True(t, getHostResp.Host.RefetchRequested)
|
|
|
|
|
|
|
|
|
|
// request a refetch for an invalid token
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/no_such_token/refetch", nil, http.StatusUnauthorized)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
|
|
|
|
|
// list device mappings for valid token
|
|
|
|
|
var listDMResp listHostDeviceMappingResponse
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/device_mapping", nil, http.StatusOK)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&listDMResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Equal(t, hosts[0].ID, listDMResp.HostID)
|
2023-12-21 17:21:39 +00:00
|
|
|
require.Len(t, listDMResp.DeviceMapping, 3)
|
|
|
|
|
require.ElementsMatch(t, listDMResp.DeviceMapping, []*fleet.HostDeviceMapping{
|
|
|
|
|
{Email: "a@b.c", Source: fleet.DeviceMappingGoogleChromeProfiles},
|
|
|
|
|
{Email: "b@b.c", Source: fleet.DeviceMappingGoogleChromeProfiles},
|
|
|
|
|
{Email: "c@b.c", Source: fleet.DeviceMappingCustomReplacement},
|
|
|
|
|
})
|
2022-07-18 17:22:49 +00:00
|
|
|
devDMs := listDMResp.DeviceMapping
|
|
|
|
|
|
|
|
|
|
// compare response with standard list device mapping API for that same host
|
|
|
|
|
listDMResp = listHostDeviceMappingResponse{}
|
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/device_mapping", hosts[0].ID), nil, http.StatusOK, &listDMResp)
|
|
|
|
|
require.Equal(t, hosts[0].ID, listDMResp.HostID)
|
|
|
|
|
require.Equal(t, devDMs, listDMResp.DeviceMapping)
|
|
|
|
|
|
|
|
|
|
// list device mappings for invalid token
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/no_such_token/device_mapping", nil, http.StatusUnauthorized)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
|
|
|
|
|
// get macadmins for valid token
|
2022-08-10 19:15:01 +00:00
|
|
|
var getMacadm macadminsDataResponse
|
2022-07-18 17:22:49 +00:00
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/macadmins", nil, http.StatusOK)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getMacadm))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Equal(t, "1.3.0", getMacadm.Macadmins.Munki.Version)
|
|
|
|
|
devMacadm := getMacadm.Macadmins
|
|
|
|
|
|
|
|
|
|
// compare response with standard macadmins API for that same host
|
2022-08-10 19:15:01 +00:00
|
|
|
getMacadm = macadminsDataResponse{}
|
2022-07-18 17:22:49 +00:00
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/macadmins", hosts[0].ID), nil, http.StatusOK, &getMacadm)
|
|
|
|
|
require.Equal(t, devMacadm, getMacadm.Macadmins)
|
|
|
|
|
|
|
|
|
|
// get macadmins for invalid token
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/no_such_token/macadmins", nil, http.StatusUnauthorized)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
|
|
|
|
|
// response includes license info
|
|
|
|
|
getHostResp = getDeviceHostResponse{}
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getHostResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.NotNil(t, getHostResp.License)
|
|
|
|
|
require.Equal(t, getHostResp.License.Tier, "free")
|
|
|
|
|
|
|
|
|
|
// device policies are not accessible for free endpoints
|
|
|
|
|
listPoliciesResp := listDevicePoliciesResponse{}
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/policies", nil, http.StatusPaymentRequired)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getHostResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-07-18 17:22:49 +00:00
|
|
|
require.Nil(t, listPoliciesResp.Policies)
|
2022-10-26 19:17:11 +00:00
|
|
|
|
|
|
|
|
// /device/desktop is not accessible for free endpoints
|
|
|
|
|
getDesktopResp := fleetDesktopResponse{}
|
|
|
|
|
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusPaymentRequired)
|
2022-12-05 22:50:49 +00:00
|
|
|
require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
|
|
|
|
|
require.NoError(t, res.Body.Close())
|
2022-10-26 19:17:11 +00:00
|
|
|
require.Nil(t, getDesktopResp.FailingPolicies)
|
2022-07-18 17:22:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestDefaultTransparencyURL tests that Fleet Free licensees are restricted to the default transparency url.
|
|
|
|
|
func (s *integrationTestSuite) TestDefaultTransparencyURL() {
|
|
|
|
|
t := s.T()
|
|
|
|
|
|
|
|
|
|
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
|
|
|
|
DetailUpdatedAt: time.Now(),
|
|
|
|
|
LabelUpdatedAt: time.Now(),
|
|
|
|
|
PolicyUpdatedAt: time.Now(),
|
|
|
|
|
SeenTime: time.Now().Add(-1 * time.Minute),
|
2022-12-26 21:32:39 +00:00
|
|
|
OsqueryHostID: ptr.String(t.Name()),
|
|
|
|
|
NodeKey: ptr.String(t.Name()),
|
2022-07-18 17:22:49 +00:00
|
|
|
UUID: uuid.New().String(),
|
|
|
|
|
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
|
|
|
|
|
Platform: "darwin",
|
|
|
|
|
})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// create device token for host
|
|
|
|
|
token := "token_test_default_transparency_url"
|
|
|
|
|
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
|
|
|
|
|
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, host.ID, token)
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// confirm initial default url
|
|
|
|
|
acResp := appConfigResponse{}
|
|
|
|
|
s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
|
|
|
|
|
require.NotNil(t, acResp)
|
|
|
|
|
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
|
|
|
|
|
|
|
|
|
|
// confirm device endpoint returns initial default url
|
|
|
|
|
deviceResp := &transparencyURLResponse{}
|
|
|
|
|
rawResp := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect)
|
2022-12-05 22:50:49 +00:00
|
|
|
json.NewDecoder(rawResp.Body).Decode(deviceResp) //nolint:errcheck
|
|
|
|
|
rawResp.Body.Close() //nolint:errcheck
|
2022-07-18 17:22:49 +00:00
|
|
|
require.NoError(t, deviceResp.Err)
|
|
|
|
|
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
|
|
|
|
|
|
|
|
|
// empty string applies default url
|
|
|
|
|
acResp = appConfigResponse{}
|
2022-09-19 17:53:44 +00:00
|
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{"fleet_desktop": {"transparency_url":""}}`), http.StatusOK, &acResp)
|
2022-07-18 17:22:49 +00:00
|
|
|
require.NotNil(t, acResp)
|
|
|
|
|
require.Equal(t, fleet.DefaultTransparencyURL, acResp.FleetDesktop.TransparencyURL)
|
|
|
|
|
|
|
|
|
|
// device endpoint returns default url
|
|
|
|
|
deviceResp = &transparencyURLResponse{}
|
|
|
|
|
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect)
|
2022-12-05 22:50:49 +00:00
|
|
|
json.NewDecoder(rawResp.Body).Decode(deviceResp) //nolint:errcheck
|
|
|
|
|
rawResp.Body.Close() //nolint:errcheck
|
2022-07-18 17:22:49 +00:00
|
|
|
require.NoError(t, deviceResp.Err)
|
|
|
|
|
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
|
|
|
|
|
|
|
|
|
// modify transparency url with custom url fails
|
|
|
|
|
acResp = appConfigResponse{}
|
|
|
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: "customURL"}}, http.StatusUnprocessableEntity, &acResp)
|
|
|
|
|
|
|
|
|
|
// device endpoint still returns default url
|
|
|
|
|
deviceResp = &transparencyURLResponse{}
|
|
|
|
|
rawResp = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/transparency", nil, http.StatusTemporaryRedirect)
|
2022-12-05 22:50:49 +00:00
|
|
|
json.NewDecoder(rawResp.Body).Decode(deviceResp) //nolint:errcheck
|
|
|
|
|
rawResp.Body.Close() //nolint:errcheck
|
2022-07-18 17:22:49 +00:00
|
|
|
require.NoError(t, deviceResp.Err)
|
|
|
|
|
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-26 18:03:50 +00:00
|
|
|
func clearRedisKey(t *testing.T, redisPool fleet.RedisPool, key string) {
|
|
|
|
|
conn := redis.ConfigureDoer(redisPool, redisPool.Get())
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
|
|
_, err := conn.Do("DEL", key)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-15 19:41:04 +00:00
|
|
|
func (s *integrationTestSuite) TestRateLimitOfEndpoints() {
|
2022-07-18 17:22:49 +00:00
|
|
|
headers := map[string]string{
|
|
|
|
|
"X-Forwarded-For": "1.2.3.4",
|
|
|
|
|
}
|
2023-06-15 19:41:04 +00:00
|
|
|
|
2025-09-26 18:03:50 +00:00
|
|
|
// Clear any previous usage of forgot_password in the test suite to start from scatch.
|
|
|
|
|
clearKeys := func() {
|
|
|
|
|
clearRedisKey(s.T(), s.redisPool, "ratelimit::forgot_password")
|
|
|
|
|
}
|
|
|
|
|
clearKeys()
|
|
|
|
|
s.T().Cleanup(clearKeys)
|
|
|
|
|
|
2023-06-15 19:41:04 +00:00
|
|
|
testCases := []struct {
|
|
|
|
|
endpoint string
|
|
|
|
|
verb string
|
|
|
|
|
payload interface{}
|
|
|
|
|
burst int
|
|
|
|
|
status int
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
endpoint: "/api/latest/fleet/forgot_password",
|
|
|
|
|
verb: "POST",
|
|
|
|
|
payload: forgotPasswordRequest{Email: "some@one.com"},
|
2025-09-26 18:03:50 +00:00
|
|
|
burst: forgotPasswordRateLimitMaxBurst + 1,
|
2023-06-15 19:41:04 +00:00
|
|
|
status: http.StatusAccepted,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-25 21:27:58 +00:00
|
|
|
// Mock working SMTP for password reset
|
|
|
|
|
config, err := s.ds.AppConfig(context.Background())
|
|
|
|
|
require.NoError(s.T(), err)
|
|
|
|
|
config.SMTPSettings.SMTPConfigured = true
|
|
|
|
|
require.NoError(s.T(), s.ds.SaveAppConfig(context.Background(), config))
|
|
|
|
|
|
2023-06-15 19:41:04 +00:00
|
|
|
for _, tCase := range testCases {
|
|
|
|
|
b, err := json.Marshal(tCase.payload)
|
|
|
|
|
require.NoError(s.T(), err)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < tCase.burst; i++ {
|
|
|
|
|
s.DoRawWithHeaders(tCase.verb, tCase.endpoint, b, tCase.status, headers).Body.Close()
|
|
|
|
|
}
|
|
|
|
|
s.DoRawWithHeaders(tCase.verb, tCase.endpoint, b, http.StatusTooManyRequests, headers).Body.Close()
|
2022-07-18 17:22:49 +00:00
|
|
|
}
|
2025-02-25 21:27:58 +00:00
|
|
|
|
|
|
|
|
// Disable it again because integration tests leak state like a sieve
|
|
|
|
|
config, err = s.ds.AppConfig(context.Background())
|
|
|
|
|
require.NoError(s.T(), err)
|
|
|
|
|
config.SMTPSettings.SMTPConfigured = false
|
|
|
|
|
require.NoError(s.T(), s.ds.SaveAppConfig(context.Background(), config))
|
2022-07-18 17:22:49 +00:00
|
|
|
}
|
2023-08-24 16:04:27 +00:00
|
|
|
|
|
|
|
|
func (s *integrationTestSuite) TestErrorReporting() {
|
|
|
|
|
t := s.T()
|
|
|
|
|
|
|
|
|
|
hosts := s.createHosts(t)
|
|
|
|
|
token := "much_valid"
|
|
|
|
|
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
|
|
|
|
|
_, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, hosts[0].ID, token)
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// invalid token is unauthorized
|
|
|
|
|
res := s.DoRawNoAuth("POST", "/api/latest/fleet/device/no_such_token/debug/errors", []byte("{}"), http.StatusUnauthorized)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
// invalid request body is a bad request
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", []byte("{},{}"), http.StatusBadRequest)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
|
|
|
|
data := make(map[string]interface{})
|
|
|
|
|
for i := int64(0); i < (maxFleetdErrorReportSize+1024)/20; i++ {
|
|
|
|
|
key := fmt.Sprintf("key%d", i)
|
|
|
|
|
value := fmt.Sprintf("value%d", i)
|
|
|
|
|
data[key] = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", jsonData, http.StatusBadRequest)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
|
2024-06-21 12:12:06 +00:00
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", []byte("{}"), http.StatusOK)
|
2023-08-24 16:04:27 +00:00
|
|
|
res.Body.Close()
|
|
|
|
|
|
2024-10-31 19:24:42 +00:00
|
|
|
// Clear errors in error store
|
|
|
|
|
s.Do("GET", "/debug/errors", nil, http.StatusOK, "flush", "true")
|
|
|
|
|
|
2023-08-24 16:04:27 +00:00
|
|
|
testTime, err := time.Parse(time.RFC3339, "1969-06-19T21:44:05Z")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
ferr := fleet.FleetdError{
|
2024-10-31 19:24:42 +00:00
|
|
|
Vital: true,
|
2023-08-24 16:04:27 +00:00
|
|
|
ErrorSource: "orbit",
|
|
|
|
|
ErrorSourceVersion: "1.1.1",
|
|
|
|
|
ErrorTimestamp: testTime,
|
|
|
|
|
ErrorMessage: "test message",
|
|
|
|
|
ErrorAdditionalInfo: map[string]any{"foo": "bar"},
|
|
|
|
|
}
|
|
|
|
|
errBytes, err := json.Marshal(ferr)
|
|
|
|
|
require.NoError(t, err)
|
2024-06-21 12:12:06 +00:00
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", errBytes, http.StatusOK)
|
2023-08-24 16:04:27 +00:00
|
|
|
res.Body.Close()
|
2024-10-31 19:24:42 +00:00
|
|
|
time.Sleep(100 * time.Millisecond) // give time for the error to be saved
|
|
|
|
|
|
|
|
|
|
// Check that error was logged.
|
|
|
|
|
var errors []map[string]interface{}
|
|
|
|
|
s.DoJSON("GET", "/debug/errors", nil, http.StatusOK, &errors)
|
|
|
|
|
require.Len(t, errors, 1)
|
|
|
|
|
expectedCount := 1
|
|
|
|
|
|
|
|
|
|
checkError := func(errorItem map[string]interface{}, expectedCount int) {
|
|
|
|
|
assert.EqualValues(t, expectedCount, errorItem["count"])
|
|
|
|
|
errChain, ok := errorItem["chain"].([]interface{})
|
|
|
|
|
require.True(t, ok, fmt.Sprintf("%T", errorItem["chain"]))
|
|
|
|
|
require.Len(t, errChain, 2)
|
|
|
|
|
errChain0, ok := errChain[0].(map[string]interface{})
|
|
|
|
|
require.True(t, ok, fmt.Sprintf("%T", errChain[0]))
|
|
|
|
|
assert.EqualValues(t, "test message", errChain0["message"])
|
|
|
|
|
errChain1, ok := errChain[1].(map[string]interface{})
|
|
|
|
|
require.True(t, ok, fmt.Sprintf("%T", errChain[1]))
|
|
|
|
|
|
|
|
|
|
// Check that the exact fleetd error can be retrieved.
|
|
|
|
|
b, err := json.Marshal(errChain1["data"])
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
var receivedErr fleet.FleetdError
|
|
|
|
|
require.NoError(t, json.Unmarshal(b, &receivedErr))
|
|
|
|
|
assert.EqualValues(t, ferr, receivedErr)
|
|
|
|
|
}
|
|
|
|
|
checkError(errors[0], expectedCount)
|
|
|
|
|
|
|
|
|
|
// Make sure metadata is present when error is aggregated.
|
|
|
|
|
srvCtx := s.server.Config.BaseContext(nil)
|
|
|
|
|
aggRaw, err := ctxerr.Aggregate(srvCtx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
var errorAgg []ctxerr.ErrorAgg
|
|
|
|
|
require.NoError(t, json.Unmarshal(aggRaw, &errorAgg))
|
|
|
|
|
require.Len(t, errorAgg, 1)
|
|
|
|
|
assert.EqualValues(t, expectedCount, errorAgg[0].Count)
|
|
|
|
|
var receivedErr fleet.FleetdError
|
|
|
|
|
require.NoError(t, json.Unmarshal(errorAgg[0].Metadata, &receivedErr))
|
|
|
|
|
assert.EqualValues(t, ferr.ErrorSource, receivedErr.ErrorSource)
|
|
|
|
|
assert.EqualValues(t, ferr.ErrorSourceVersion, receivedErr.ErrorSourceVersion)
|
|
|
|
|
assert.EqualValues(t, ferr.ErrorMessage, receivedErr.ErrorMessage)
|
|
|
|
|
assert.EqualValues(t, ferr.ErrorAdditionalInfo, receivedErr.ErrorAdditionalInfo)
|
|
|
|
|
assert.NotEqual(t, ferr.ErrorTimestamp, receivedErr.ErrorTimestamp) // not included
|
|
|
|
|
|
|
|
|
|
// Sending error again should increment the count.
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", errBytes, http.StatusOK)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
expectedCount++
|
|
|
|
|
|
|
|
|
|
// Changing the timestamp should only increment the count.
|
|
|
|
|
testTime2, err := time.Parse(time.RFC3339, "2024-10-30T09:44:05Z")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
ferr = fleet.FleetdError{
|
|
|
|
|
Vital: true,
|
|
|
|
|
ErrorSource: "orbit",
|
|
|
|
|
ErrorSourceVersion: "1.1.1",
|
|
|
|
|
ErrorTimestamp: testTime2,
|
|
|
|
|
ErrorMessage: "test message",
|
|
|
|
|
ErrorAdditionalInfo: map[string]any{"foo": "bar"},
|
|
|
|
|
}
|
|
|
|
|
errBytes, err = json.Marshal(ferr)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", errBytes, http.StatusOK)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
expectedCount++
|
|
|
|
|
time.Sleep(100 * time.Millisecond) // give time for the error(s) to be saved
|
|
|
|
|
|
|
|
|
|
// Check that error was logged.
|
|
|
|
|
s.DoJSON("GET", "/debug/errors", nil, http.StatusOK, &errors)
|
|
|
|
|
require.Len(t, errors, 1)
|
|
|
|
|
checkError(errors[0], expectedCount)
|
|
|
|
|
|
|
|
|
|
// Changing vital flag should NOT create a new error, but will overwrite the existing one.
|
|
|
|
|
ferr = fleet.FleetdError{
|
|
|
|
|
Vital: false,
|
|
|
|
|
ErrorSource: "orbit",
|
|
|
|
|
ErrorSourceVersion: "1.1.1",
|
|
|
|
|
ErrorTimestamp: testTime,
|
|
|
|
|
ErrorMessage: "test message",
|
|
|
|
|
ErrorAdditionalInfo: map[string]any{"foo": "bar"},
|
|
|
|
|
}
|
|
|
|
|
errBytes, err = json.Marshal(ferr)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", errBytes, http.StatusOK)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
expectedCount++
|
|
|
|
|
time.Sleep(100 * time.Millisecond) // give time for the error(s) to be saved
|
|
|
|
|
|
|
|
|
|
aggRaw, err = ctxerr.Aggregate(srvCtx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
errorAgg = nil
|
|
|
|
|
require.NoError(t, json.Unmarshal(aggRaw, &errorAgg))
|
|
|
|
|
require.Len(t, errorAgg, 1)
|
|
|
|
|
assert.EqualValues(t, expectedCount, errorAgg[0].Count)
|
|
|
|
|
// Since the error is not vital, the metadata should be empty.
|
|
|
|
|
assert.Empty(t, string(errorAgg[0].Metadata))
|
|
|
|
|
|
|
|
|
|
// Changing additional info should create a new error
|
|
|
|
|
ferr = fleet.FleetdError{
|
|
|
|
|
Vital: true,
|
|
|
|
|
ErrorSource: "orbit",
|
|
|
|
|
ErrorSourceVersion: "1.1.1",
|
|
|
|
|
ErrorTimestamp: testTime,
|
|
|
|
|
ErrorMessage: "test message",
|
|
|
|
|
ErrorAdditionalInfo: map[string]any{"foo": "bar2"},
|
|
|
|
|
}
|
|
|
|
|
errBytes, err = json.Marshal(ferr)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
res = s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/debug/errors", errBytes, http.StatusOK)
|
|
|
|
|
res.Body.Close()
|
|
|
|
|
time.Sleep(100 * time.Millisecond) // give time for the error(s) to be saved
|
|
|
|
|
|
|
|
|
|
s.DoJSON("GET", "/debug/errors", nil, http.StatusOK, &errors)
|
|
|
|
|
require.Len(t, errors, 2)
|
2023-08-24 16:04:27 +00:00
|
|
|
}
|
2025-09-26 18:03:50 +00:00
|
|
|
|
|
|
|
|
func (s *integrationEnterpriseTestSuite) TestRateLimitOfDesktopEndpoints() {
|
|
|
|
|
createHostAndDeviceToken(s.T(), s.ds, "valid_token")
|
|
|
|
|
|
|
|
|
|
// Clear any previous usage of forgot_password in the test suite to start from scatch.
|
|
|
|
|
clearKeys := func() {
|
|
|
|
|
clearRedisKey(s.T(), s.redisPool, "ipbanner::{127.0.0.1}::banned")
|
|
|
|
|
clearRedisKey(s.T(), s.redisPool, "ipbanner::{127.0.0.1}::count")
|
|
|
|
|
}
|
|
|
|
|
clearKeys()
|
|
|
|
|
s.T().Cleanup(clearKeys)
|
|
|
|
|
|
|
|
|
|
allowedConsecutiveFailuresCount := 4
|
|
|
|
|
allowedConsecutiveFailuresTimeWindow := 2 * time.Second
|
|
|
|
|
banDuration := 2 * time.Second
|
|
|
|
|
redis.SetIPBannerTestValues(allowedConsecutiveFailuresCount, allowedConsecutiveFailuresTimeWindow, banDuration)
|
|
|
|
|
s.T().Cleanup(redis.UnsetIPBannerTestValues)
|
|
|
|
|
|
|
|
|
|
// Test that consecutive invalid requests get banned (for IP=127.0.0.1).
|
|
|
|
|
// Test different endpoints coming from the same IP.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("POST", "/api/latest/fleet/device/invalid_token/refetch", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/transparency", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("HEAD", "/api/latest/fleet/device/invalid_token/ping", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
|
|
|
|
|
// A valid request from another IP does not clear the status of the IP 127.0.0.1 (banned).
|
|
|
|
|
s.DoRawWithHeaders("GET", "/api/latest/fleet/device/valid_token/desktop", nil, http.StatusOK, map[string]string{
|
|
|
|
|
"X-Forwarded-For": "1.2.3.4",
|
|
|
|
|
}).Body.Close()
|
|
|
|
|
// 127.0.0.1 IP should be banned.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusTooManyRequests).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusTooManyRequests).Body.Close()
|
|
|
|
|
// Wait for the ban window to finish, which should clear the ban on the IP.
|
|
|
|
|
time.Sleep(banDuration + 500*time.Millisecond)
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
|
|
|
|
|
// Clear rate limiting.
|
|
|
|
|
clearKeys()
|
|
|
|
|
|
|
|
|
|
// Test that a successful request clears the failing count.
|
|
|
|
|
for range allowedConsecutiveFailuresCount - 1 {
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
}
|
|
|
|
|
// Valid request clears the failing count.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/valid_token/desktop", nil, http.StatusOK).Body.Close()
|
|
|
|
|
// A new failing request is not banned.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
|
|
|
|
|
// Test that consecutive invalid requests in a time window bigger than the configured one does not ban the IP (for IP=127.0.0.1).
|
|
|
|
|
// Test different endpoints coming from the same IP.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("POST", "/api/latest/fleet/device/invalid_token/refetch", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
time.Sleep(allowedConsecutiveFailuresTimeWindow + 500*time.Millisecond)
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/transparency", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
s.DoRawNoAuth("HEAD", "/api/latest/fleet/device/invalid_token/ping", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
// A new failing request is not banned.
|
|
|
|
|
s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/desktop", nil, http.StatusUnauthorized).Body.Close()
|
|
|
|
|
}
|