Set authz checked when rate limiting device endpoints (#6702)
* Set authz checked when rate limiting device endpoints * Unexport var and attempt to fix flaky test
This commit is contained in:
@@ -53,7 +53,7 @@ func TestCollector(t *testing.T) {
|
||||
|
||||
// running at each 10ms ±10% for 100ms, min 9, max 11 but stay on the
|
||||
// safe side, especially for min.
|
||||
require.GreaterOrEqual(t, countHandler, 5)
|
||||
require.GreaterOrEqual(t, countHandler, 4)
|
||||
require.LessOrEqual(t, countHandler, 12)
|
||||
|
||||
stats := coll.ReadStats()
|
||||
|
||||
@@ -219,6 +219,11 @@ func addMetrics(r *mux.Router) {
|
||||
r.Walk(walkFn)
|
||||
}
|
||||
|
||||
// desktopRateLimitMaxBurst is the max burst used for device request rate limiting.
|
||||
//
|
||||
// Defined as const to be used in tests.
|
||||
const desktopRateLimitMaxBurst = 100
|
||||
|
||||
func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetConfig,
|
||||
logger kitlog.Logger, limitStore throttled.GCRAStore, opts []kithttp.ServerOption,
|
||||
extra extraHandlerOpts,
|
||||
@@ -401,7 +406,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
||||
de := newDeviceAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
||||
// We allow a quota of 720 because in the onboarding of a Fleet Desktop takes a few tries until it authenticates
|
||||
// properly
|
||||
desktopQuota := throttled.RateQuota{MaxRate: throttled.PerHour(720), MaxBurst: 100}
|
||||
desktopQuota := throttled.RateQuota{MaxRate: throttled.PerHour(720), MaxBurst: desktopRateLimitMaxBurst}
|
||||
de.WithCustomMiddleware(
|
||||
errorLimiter.Limit("get_device_host", desktopQuota),
|
||||
).GET("/api/_version_/fleet/device/{token}", getDeviceHostEndpoint, getDeviceHostRequest{})
|
||||
|
||||
@@ -4610,202 +4610,6 @@ func (s *integrationTestSuite) TestPasswordReset() {
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
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"
|
||||
err = s.ds.SaveAppConfig(context.Background(), ac)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create some mappings and MDM/Munki data
|
||||
s.ds.ReplaceHostDeviceMapping(context.Background(), hosts[0].ID, []*fleet.HostDeviceMapping{
|
||||
{HostID: hosts[0].ID, Email: "a@b.c", Source: "google_chrome_profiles"},
|
||||
{HostID: hosts[0].ID, Email: "b@b.c", Source: "google_chrome_profiles"},
|
||||
})
|
||||
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), hosts[0].ID, true, "url", false))
|
||||
require.NoError(t, s.ds.SetOrUpdateMunkiVersion(context.Background(), hosts[0].ID, "1.3.0"))
|
||||
// create a battery for hosts[0]
|
||||
require.NoError(t, s.ds.ReplaceHostBatteries(context.Background(), hosts[0].ID, []*fleet.HostBattery{
|
||||
{HostID: hosts[0].ID, SerialNumber: "a", CycleCount: 1, Health: "Good"},
|
||||
}))
|
||||
|
||||
// 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()
|
||||
|
||||
// get host with valid token
|
||||
var getHostResp getDeviceHostResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
require.Equal(t, hosts[0].ID, getHostResp.Host.ID)
|
||||
require.False(t, getHostResp.Host.RefetchRequested)
|
||||
require.Equal(t, "http://example.com/logo", getHostResp.OrgLogoURL)
|
||||
require.Nil(t, getHostResp.Host.Policies)
|
||||
require.NotNil(t, getHostResp.Host.Batteries)
|
||||
require.Equal(t, &fleet.HostBattery{CycleCount: 1, Health: "Good"}, (*getHostResp.Host.Batteries)[0])
|
||||
hostDevResp := getHostResp.Host
|
||||
|
||||
// make request for same host on the host details API endpoint, responses should match, except for policies
|
||||
getHostResp = getDeviceHostResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hosts[0].ID), nil, http.StatusOK, &getHostResp)
|
||||
getHostResp.Host.Policies = nil
|
||||
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)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// list device mappings for valid token
|
||||
var listDMResp listHostDeviceMappingResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/device_mapping", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&listDMResp)
|
||||
res.Body.Close()
|
||||
require.Equal(t, hosts[0].ID, listDMResp.HostID)
|
||||
require.Len(t, listDMResp.DeviceMapping, 2)
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// get macadmins for valid token
|
||||
var getMacadm getMacadminsDataResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/macadmins", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getMacadm)
|
||||
res.Body.Close()
|
||||
require.Equal(t, "1.3.0", getMacadm.Macadmins.Munki.Version)
|
||||
devMacadm := getMacadm.Macadmins
|
||||
|
||||
// compare response with standard macadmins API for that same host
|
||||
getMacadm = getMacadminsDataResponse{}
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// response includes license info
|
||||
getHostResp = getDeviceHostResponse{}
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
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)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
require.Nil(t, listPoliciesResp.Policies)
|
||||
|
||||
// get list of api features
|
||||
apiFeaturesResp := deviceAPIFeaturesResponse{}
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/api_features", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&apiFeaturesResp)
|
||||
res.Body.Close()
|
||||
require.Nil(t, apiFeaturesResp.Err)
|
||||
require.NotNil(t, apiFeaturesResp.Features)
|
||||
}
|
||||
|
||||
// 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),
|
||||
OsqueryHostID: t.Name(),
|
||||
NodeKey: t.Name(),
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
||||
|
||||
// empty string applies default url
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: ""}}, http.StatusOK, &acResp)
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestModifyUser() {
|
||||
t := s.T()
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"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"
|
||||
err = s.ds.SaveAppConfig(context.Background(), ac)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create some mappings and MDM/Munki data
|
||||
s.ds.ReplaceHostDeviceMapping(context.Background(), hosts[0].ID, []*fleet.HostDeviceMapping{
|
||||
{HostID: hosts[0].ID, Email: "a@b.c", Source: "google_chrome_profiles"},
|
||||
{HostID: hosts[0].ID, Email: "b@b.c", Source: "google_chrome_profiles"},
|
||||
})
|
||||
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), hosts[0].ID, true, "url", false))
|
||||
require.NoError(t, s.ds.SetOrUpdateMunkiVersion(context.Background(), hosts[0].ID, "1.3.0"))
|
||||
// create a battery for hosts[0]
|
||||
require.NoError(t, s.ds.ReplaceHostBatteries(context.Background(), hosts[0].ID, []*fleet.HostBattery{
|
||||
{HostID: hosts[0].ID, SerialNumber: "a", CycleCount: 1, Health: "Good"},
|
||||
}))
|
||||
|
||||
// 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()
|
||||
|
||||
// get host with valid token
|
||||
var getHostResp getDeviceHostResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
require.Equal(t, hosts[0].ID, getHostResp.Host.ID)
|
||||
require.False(t, getHostResp.Host.RefetchRequested)
|
||||
require.Equal(t, "http://example.com/logo", getHostResp.OrgLogoURL)
|
||||
require.Nil(t, getHostResp.Host.Policies)
|
||||
require.NotNil(t, getHostResp.Host.Batteries)
|
||||
require.Equal(t, &fleet.HostBattery{CycleCount: 1, Health: "Good"}, (*getHostResp.Host.Batteries)[0])
|
||||
hostDevResp := getHostResp.Host
|
||||
|
||||
// make request for same host on the host details API endpoint, responses should match, except for policies
|
||||
getHostResp = getDeviceHostResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hosts[0].ID), nil, http.StatusOK, &getHostResp)
|
||||
getHostResp.Host.Policies = nil
|
||||
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)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// list device mappings for valid token
|
||||
var listDMResp listHostDeviceMappingResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/device_mapping", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&listDMResp)
|
||||
res.Body.Close()
|
||||
require.Equal(t, hosts[0].ID, listDMResp.HostID)
|
||||
require.Len(t, listDMResp.DeviceMapping, 2)
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// get macadmins for valid token
|
||||
var getMacadm getMacadminsDataResponse
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/macadmins", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getMacadm)
|
||||
res.Body.Close()
|
||||
require.Equal(t, "1.3.0", getMacadm.Macadmins.Munki.Version)
|
||||
devMacadm := getMacadm.Macadmins
|
||||
|
||||
// compare response with standard macadmins API for that same host
|
||||
getMacadm = getMacadminsDataResponse{}
|
||||
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)
|
||||
res.Body.Close()
|
||||
|
||||
// response includes license info
|
||||
getHostResp = getDeviceHostResponse{}
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token, nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
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)
|
||||
json.NewDecoder(res.Body).Decode(&getHostResp)
|
||||
res.Body.Close()
|
||||
require.Nil(t, listPoliciesResp.Policies)
|
||||
|
||||
// get list of api features
|
||||
apiFeaturesResp := deviceAPIFeaturesResponse{}
|
||||
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/api_features", nil, http.StatusOK)
|
||||
json.NewDecoder(res.Body).Decode(&apiFeaturesResp)
|
||||
res.Body.Close()
|
||||
require.Nil(t, apiFeaturesResp.Err)
|
||||
require.NotNil(t, apiFeaturesResp.Features)
|
||||
}
|
||||
|
||||
// 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),
|
||||
OsqueryHostID: t.Name(),
|
||||
NodeKey: t.Name(),
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
||||
|
||||
// empty string applies default url
|
||||
acResp = appConfigResponse{}
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", fleet.AppConfig{FleetDesktop: fleet.FleetDesktopSettings{TransparencyURL: ""}}, http.StatusOK, &acResp)
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
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)
|
||||
json.NewDecoder(rawResp.Body).Decode(deviceResp)
|
||||
rawResp.Body.Close()
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.Equal(t, fleet.DefaultTransparencyURL, rawResp.Header.Get("Location"))
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestDesktopRateLimit() {
|
||||
headers := map[string]string{
|
||||
"X-Forwarded-For": "1.2.3.4",
|
||||
}
|
||||
for i := 0; i < desktopRateLimitMaxBurst+1; i++ { // rate limiting off-by-one
|
||||
s.DoRawWithHeaders("GET", "/api/latest/fleet/device/"+uuid.NewString(), nil, http.StatusUnauthorized, headers).Body.Close()
|
||||
}
|
||||
s.DoRawWithHeaders("GET", "/api/latest/fleet/device/"+uuid.NewString(), nil, http.StatusTooManyRequests, headers).Body.Close()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
@@ -80,6 +81,10 @@ func (m *ErrorMiddleware) Limit(keyName string, quota throttled.RateQuota) endpo
|
||||
return nil, ctxerr.Wrap(ctx, err, "check rate limit")
|
||||
}
|
||||
if result.Remaining == 0 {
|
||||
// We need to set authentication as checked, otherwise we end up returning HTTP 500 errors.
|
||||
if az, ok := authz_ctx.FromContext(ctx); ok {
|
||||
az.SetChecked()
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, &ratelimitError{result: result})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user