enable controlled rollout of features by teams (#7408)

This commit is contained in:
Roberto Dip 2022-08-30 08:13:09 -03:00 committed by GitHub
parent a2dc154803
commit eeefe2fab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 621 additions and 79 deletions

View File

@ -1 +1,2 @@
* Renamed the `host_settings` config to `features`
* Renamed the `host_settings` config to `features`.
* Teams: Added the ability to apply custom `features` settings to each team.

View File

@ -119,6 +119,7 @@ func TestGetTeams(t *testing.T) {
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license})
agentOpts := json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)
additionalQueries := json.RawMessage(`{"foo":"bar"}`)
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
created_at, err := time.Parse(time.RFC3339, "1999-03-10T02:45:06.371Z")
require.NoError(t, err)
@ -129,6 +130,12 @@ func TestGetTeams(t *testing.T) {
Name: "team1",
Description: "team1 description",
UserCount: 99,
Config: fleet.TeamConfig{
Features: fleet.Features{
EnableHostUsers: true,
EnableSoftwareInventory: true,
},
},
},
{
ID: 43,
@ -138,6 +145,9 @@ func TestGetTeams(t *testing.T) {
UserCount: 87,
Config: fleet.TeamConfig{
AgentOptions: &agentOpts,
Features: fleet.Features{
AdditionalQueries: &additionalQueries,
},
},
},
}, nil
@ -158,6 +168,9 @@ spec:
team:
created_at: "1999-03-10T02:45:06.371Z"
description: team1 description
features:
enable_host_users: true
enable_software_inventory: true
host_count: 0
id: 42
integrations:
@ -185,6 +198,11 @@ spec:
foo: override
created_at: "1999-03-10T02:45:06.371Z"
description: team2 description
features:
additional_queries:
foo: bar
enable_host_users: false
enable_software_inventory: false
host_count: 0
id: 43
integrations:
@ -199,8 +217,8 @@ spec:
host_batch_size: 0
policy_ids: null
`
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"user_count":99,"host_count":0}}}
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"user_count":87,"host_count":0}}}
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":true,"enable_software_inventory":true},"user_count":99,"host_count":0}}}
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":false,"enable_software_inventory":false,"additional_queries":{"foo":"bar"}},"user_count":87,"host_count":0}}}
`
if tt.shouldHaveExpiredBanner {
expectedJson = expiredBanner.String() + expectedJson

View File

@ -464,11 +464,12 @@ If the `name` is not already associated with an existing team, this API route cr
#### Parameters
| Name | Type | In | Description |
| ------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | body | **Required.** The team's name. |
| agent_options | string | body | The agent options spec that is applied to the hosts assigned to the specified to team. These agent options completely override the global agent options specified in the [`GET /api/v1/fleet/config API route`](#get-configuration) |
| secrets | list | body | A list of plain text strings is used as the enroll secrets. Existing secrets are replaced with this list, or left unmodified if this list is empty. |
| Name | Type | In | Description |
| ------------- | ------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | body | **Required.** The team's name. |
| agent_options | string | body | The agent options spec that is applied to the hosts assigned to the specified to team. These agent options completely override the global agent options specified in the [`GET /api/v1/fleet/config API route`](#get-configuration) |
| features | object | body | The features that are applied to the hosts assigned to the specified to team. These features completely override the global features specified in the [`GET /api/v1/fleet/config API route`](#get-configuration) |
| secrets | list | body | A list of plain text strings is used as the enroll secrets. Existing secrets are replaced with this list, or left unmodified if this list is empty. |
#### Example
@ -481,6 +482,13 @@ If the `name` is not already associated with an existing team, this API route cr
"specs": [
{
"name": "Client Platform Engineering",
"features": {
"enable_host_users": false,
"enable_software_inventory": true,
"additional_queries": {
"foo": "SELECT * FROM bar;"
}
},
"agent_options": {
"spec": {
"config": {

View File

@ -171,6 +171,12 @@ kind: team
spec:
team:
name: Client Platform Engineering
features:
enable_host_users: false
enable_software_inventory: true
additional_queries:
time: SELECT * FROM time
macs: SELECT mac FROM interface_details
agent_options:
config:
decorators:

View File

@ -0,0 +1,29 @@
package service
import (
"context"
"github.com/fleetdm/fleet/v4/server/fleet"
)
// HostFeatures retrieves the features enabled for a given host.
//
// - If the host belongs to a team, it will use the team's features. When a team
// is created the features are mixed with the global config features, but from
// that point on they are independent of whatever the global config is.
// - If the host doesn't belong to a team, the app config features are used.
func (svc *Service) HostFeatures(ctx context.Context, host *fleet.Host) (*fleet.Features, error) {
if host.TeamID != nil {
features, err := svc.ds.TeamFeatures(ctx, *host.TeamID)
if err != nil {
return nil, err
}
return features, nil
}
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, err
}
return &appConfig.Features, nil
}

View File

@ -36,7 +36,7 @@ func NewService(
return nil, fmt.Errorf("new authorizer: %w", err)
}
return &Service{
eeservice := &Service{
Service: svc,
ds: ds,
logger: logger,
@ -44,5 +44,13 @@ func NewService(
clock: c,
authz: authorizer,
license: license,
}, nil
}
// Override methods that can't be easily overriden via
// embedding.
svc.SetEnterpriseOverrides(fleet.EnterpriseOverrides{
HostFeatures: eeservice.HostFeatures,
})
return eeservice, nil
}

View File

@ -28,6 +28,7 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te
team := &fleet.Team{
Config: fleet.TeamConfig{
AgentOptions: globalConfig.AgentOptions,
Features: globalConfig.Features,
},
}
@ -378,7 +379,7 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
}
}
config, err := svc.AppConfig(ctx)
appConfig, err := svc.AppConfig(ctx)
if err != nil {
return err
}
@ -400,23 +401,13 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
team, err := svc.ds.TeamByName(ctx, spec.Name)
if err != nil {
if err := ctxerr.Cause(err); err == sql.ErrNoRows {
agentOptions := spec.AgentOptions
if agentOptions == nil {
agentOptions = config.AgentOptions
}
tm, err := svc.ds.NewTeam(ctx, &fleet.Team{
Name: spec.Name,
Config: fleet.TeamConfig{
AgentOptions: agentOptions,
},
Secrets: secrets,
})
team, err := svc.createTeamFromSpec(ctx, spec, appConfig, secrets)
if err != nil {
return err
return ctxerr.Wrap(ctx, err, "creating team from spec")
}
details = append(details, activityDetail{
ID: tm.ID,
Name: tm.Name,
ID: team.ID,
Name: team.Name,
})
continue
}
@ -424,23 +415,8 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
return err
}
team.Name = spec.Name
team.Config.AgentOptions = spec.AgentOptions
if len(secrets) > 0 {
team.Secrets = secrets
}
_, err = svc.ds.SaveTeam(ctx, team)
if err != nil {
return err
}
// only replace enroll secrets if at least one is provided (#6774)
if len(secrets) > 0 {
err = svc.ds.ApplyEnrollSecrets(ctx, ptr.Uint(team.ID), secrets)
if err != nil {
return err
}
if err := svc.editTeamFromSpec(ctx, team, spec, secrets); err != nil {
return ctxerr.Wrap(ctx, err, "editing team from spec")
}
details = append(details, activityDetail{
@ -459,3 +435,46 @@ func (svc Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec)
}
return nil
}
func (svc Service) createTeamFromSpec(ctx context.Context, spec *fleet.TeamSpec, defaults *fleet.AppConfig, secrets []*fleet.EnrollSecret) (*fleet.Team, error) {
agentOptions := spec.AgentOptions
if agentOptions == nil {
agentOptions = defaults.AgentOptions
}
features := spec.Features
if features == nil {
features = &defaults.Features
}
return svc.ds.NewTeam(ctx, &fleet.Team{
Name: spec.Name,
Config: fleet.TeamConfig{
AgentOptions: agentOptions,
Features: *features,
},
Secrets: secrets,
})
}
func (svc Service) editTeamFromSpec(ctx context.Context, team *fleet.Team, spec *fleet.TeamSpec, secrets []*fleet.EnrollSecret) error {
team.Name = spec.Name
team.Config.AgentOptions = spec.AgentOptions
if spec.Features != nil {
team.Config.Features = *spec.Features
}
if len(secrets) > 0 {
team.Secrets = secrets
}
if _, err := svc.ds.SaveTeam(ctx, team); err != nil {
return err
}
// only replace enroll secrets if at least one is provided (#6774)
if len(secrets) > 0 {
if err := svc.ds.ApplyEnrollSecrets(ctx, ptr.Uint(team.ID), secrets); err != nil {
return err
}
}
return nil
}

View File

@ -21,6 +21,8 @@ const (
defaultScheduledQueriesExpiration = 1 * time.Minute
teamAgentOptionsKey = "TeamAgentOptions:team:%d"
defaultTeamAgentOptionsExpiration = 1 * time.Minute
teamFeaturesKey = "TeamFeatures:team:%d"
defaultTeamFeaturesExpiration = 1 * time.Minute
)
// cloner represents any type that can clone itself. Used by types to provide a more efficient clone method.
@ -96,6 +98,7 @@ type cachedMysql struct {
packsExp time.Duration
scheduledQueriesExp time.Duration
teamAgentOptionsExp time.Duration
teamFeaturesExp time.Duration
}
type Option func(*cachedMysql)
@ -118,6 +121,12 @@ func WithTeamAgentOptionsExpiration(d time.Duration) Option {
}
}
func WithTeamFeaturesExpiration(d time.Duration) Option {
return func(o *cachedMysql) {
o.teamFeaturesExp = d
}
}
func New(ds fleet.Datastore, opts ...Option) fleet.Datastore {
c := &cachedMysql{
Datastore: ds,
@ -125,6 +134,7 @@ func New(ds fleet.Datastore, opts ...Option) fleet.Datastore {
packsExp: defaultPacksExpiration,
scheduledQueriesExp: defaultScheduledQueriesExpiration,
teamAgentOptionsExp: defaultTeamAgentOptionsExpiration,
teamFeaturesExp: defaultTeamFeaturesExpiration,
}
for _, fn := range opts {
fn(c)
@ -228,15 +238,35 @@ func (ds *cachedMysql) TeamAgentOptions(ctx context.Context, teamID uint) (*json
return agentOptions, nil
}
func (ds *cachedMysql) TeamFeatures(ctx context.Context, teamID uint) (*fleet.Features, error) {
key := fmt.Sprintf(teamFeaturesKey, teamID)
if x, found := ds.c.Get(key); found {
if features, ok := x.(*fleet.Features); ok {
return features, nil
}
}
features, err := ds.Datastore.TeamFeatures(ctx, teamID)
if err != nil {
return nil, err
}
ds.c.Set(key, features, ds.teamFeaturesExp)
return features, nil
}
func (ds *cachedMysql) SaveTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
team, err := ds.Datastore.SaveTeam(ctx, team)
if err != nil {
return nil, err
}
key := fmt.Sprintf(teamAgentOptionsKey, team.ID)
agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, team.ID)
featuresKey := fmt.Sprintf(teamFeaturesKey, team.ID)
ds.c.Set(key, team.Config.AgentOptions, ds.teamAgentOptionsExp)
ds.c.Set(agentOptionsKey, team.Config.AgentOptions, ds.teamAgentOptionsExp)
ds.c.Set(featuresKey, &team.Config.Features, ds.teamFeaturesExp)
return team, nil
}
@ -247,9 +277,11 @@ func (ds *cachedMysql) DeleteTeam(ctx context.Context, teamID uint) error {
return err
}
key := fmt.Sprintf(teamAgentOptionsKey, teamID)
agentOptionsKey := fmt.Sprintf(teamAgentOptionsKey, teamID)
featuresKey := fmt.Sprintf(teamFeaturesKey, teamID)
ds.c.Delete(key)
ds.c.Delete(agentOptionsKey)
ds.c.Delete(featuresKey)
return nil
}

View File

@ -330,3 +330,78 @@ func TestCachedTeamAgentOptions(t *testing.T) {
_, err = ds.TeamAgentOptions(context.Background(), testTeam.ID)
require.Error(t, err)
}
func TestCachedTeamFeatures(t *testing.T) {
t.Parallel()
mockedDS := new(mock.Store)
ds := New(mockedDS, WithTeamFeaturesExpiration(100*time.Millisecond))
ao := json.RawMessage(`{}`)
aq := json.RawMessage(`{"foo": "bar"}`)
testFeatures := fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: true,
AdditionalQueries: &aq,
}
testTeam := fleet.Team{
ID: 1,
CreatedAt: time.Now(),
Name: "test",
Config: fleet.TeamConfig{
Features: testFeatures,
AgentOptions: &ao,
},
}
deleted := false
mockedDS.TeamFeaturesFunc = func(ctx context.Context, teamID uint) (*fleet.Features, error) {
if deleted {
return nil, errors.New("not found")
}
return &testFeatures, nil
}
mockedDS.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
return team, nil
}
mockedDS.DeleteTeamFunc = func(ctx context.Context, teamID uint) error {
deleted = true
return nil
}
features, err := ds.TeamFeatures(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, testFeatures, *features)
// saving a team updates features in cache
aq = json.RawMessage(`{"bar": "baz"}`)
updateFeatures := fleet.Features{
EnableHostUsers: true,
EnableSoftwareInventory: false,
AdditionalQueries: &aq,
}
updateTeam := &fleet.Team{
ID: testTeam.ID,
CreatedAt: testTeam.CreatedAt,
Name: testTeam.Name,
Config: fleet.TeamConfig{
Features: updateFeatures,
AgentOptions: &ao,
},
}
_, err = ds.SaveTeam(context.Background(), updateTeam)
require.NoError(t, err)
features, err = ds.TeamFeatures(context.Background(), testTeam.ID)
require.NoError(t, err)
require.Equal(t, updateFeatures, *features)
// deleting a team removes the features from the cache
err = ds.DeleteTeam(context.Background(), testTeam.ID)
require.NoError(t, err)
_, err = ds.TeamFeatures(context.Background(), testTeam.ID)
require.Error(t, err)
}

View File

@ -301,6 +301,20 @@ func (ds *Datastore) TeamAgentOptions(ctx context.Context, tid uint) (*json.RawM
return agentOptions, nil
}
// TeamFeatures loads the features enabled for a team.
func (ds *Datastore) TeamFeatures(ctx context.Context, tid uint) (*fleet.Features, error) {
sql := `SELECT config->'$.features' as features FROM teams WHERE id = ?`
var raw *json.RawMessage
if err := sqlx.GetContext(ctx, ds.reader, &raw, sql, tid); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get team config features")
}
var features fleet.Features
if err := json.Unmarshal(*raw, &features); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshal team config features")
}
return &features, nil
}
// DeleteIntegrationsFromTeams removes the deleted integrations from any team
// that uses it.
func (ds *Datastore) DeleteIntegrationsFromTeams(ctx context.Context, deletedIntgs fleet.Integrations) error {

View File

@ -526,6 +526,9 @@ type Datastore interface {
// TeamAgentOptions loads the agents options of a team.
TeamAgentOptions(ctx context.Context, teamID uint) (*json.RawMessage, error)
// TeamFeatures loads the features enabled for a team.
TeamFeatures(ctx context.Context, teamID uint) (*Features, error)
// SaveHostPackStats stores (and updates) the pack's scheduled queries stats of a host.
SaveHostPackStats(ctx context.Context, hostID uint, stats []PackStats) error
// AsyncBatchSaveHostsScheduledQueryStats efficiently saves a batch of hosts'

View File

@ -10,6 +10,14 @@ import (
"github.com/kolide/kit/version"
)
// EnterpriseOverrides contains the methods that can be overriden by the
// enterprise service
//
// TODO: find if there's a better way to accomplish this and standardize.
type EnterpriseOverrides struct {
HostFeatures func(context context.Context, host *Host) (*Features, error)
}
type OsqueryService interface {
EnrollAgent(
ctx context.Context, enrollSecret, hostIdentifier string, hostDetails map[string](map[string]string),
@ -42,6 +50,12 @@ type OsqueryService interface {
type Service interface {
OsqueryService
// SetEnterpriseOverrides allows the enterprise service to override specific methods
// that can't be easily overridden via embedding.
//
// TODO: find if there's a better way to accomplish this and standardize.
SetEnterpriseOverrides(overrides EnterpriseOverrides)
///////////////////////////////////////////////////////////////////////////////
// UserService contains methods for managing a Fleet User.

View File

@ -122,6 +122,7 @@ type TeamConfig struct {
AgentOptions *json.RawMessage `json:"agent_options,omitempty"`
WebhookSettings TeamWebhookSettings `json:"webhook_settings"`
Integrations TeamIntegrations `json:"integrations"`
Features Features `json:"features"`
}
type TeamWebhookSettings struct {
@ -252,4 +253,5 @@ type TeamSpec struct {
Name string `json:"name"`
AgentOptions *json.RawMessage `json:"agent_options"`
Secrets []EnrollSecret `json:"secrets"`
Features *Features `json:"features"`
}

View File

@ -393,6 +393,8 @@ type UpdateHostOsqueryIntervalsFunc func(ctx context.Context, hostID uint, inter
type TeamAgentOptionsFunc func(ctx context.Context, teamID uint) (*json.RawMessage, error)
type TeamFeaturesFunc func(ctx context.Context, teamID uint) (*fleet.Features, error)
type SaveHostPackStatsFunc func(ctx context.Context, hostID uint, stats []fleet.PackStats) error
type AsyncBatchSaveHostsScheduledQueryStatsFunc func(ctx context.Context, stats map[uint][]fleet.ScheduledQueryStats, batchSize int) (int, error)
@ -1012,6 +1014,9 @@ type DataStore struct {
TeamAgentOptionsFunc TeamAgentOptionsFunc
TeamAgentOptionsFuncInvoked bool
TeamFeaturesFunc TeamFeaturesFunc
TeamFeaturesFuncInvoked bool
SaveHostPackStatsFunc SaveHostPackStatsFunc
SaveHostPackStatsFuncInvoked bool
@ -2035,6 +2040,11 @@ func (s *DataStore) TeamAgentOptions(ctx context.Context, teamID uint) (*json.Ra
return s.TeamAgentOptionsFunc(ctx, teamID)
}
func (s *DataStore) TeamFeatures(ctx context.Context, teamID uint) (*fleet.Features, error) {
s.TeamFeaturesFuncInvoked = true
return s.TeamFeaturesFunc(ctx, teamID)
}
func (s *DataStore) SaveHostPackStats(ctx context.Context, hostID uint, stats []fleet.PackStats) error {
s.SaveHostPackStatsFuncInvoked = true
return s.SaveHostPackStatsFunc(ctx, hostID, stats)

View File

@ -603,3 +603,15 @@ func encodePEMCertificate(buf io.Writer, cert *x509.Certificate) error {
}
return pem.Encode(buf, block)
}
func (svc *Service) HostFeatures(ctx context.Context, host *fleet.Host) (*fleet.Features, error) {
if svc.EnterpriseOverrides != nil {
return svc.EnterpriseOverrides.HostFeatures(ctx, host)
}
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, err
}
return &appConfig.Features, nil
}

View File

@ -13,11 +13,13 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -32,17 +34,21 @@ type integrationEnterpriseTestSuite struct {
withServer
suite.Suite
redisPool fleet.RedisPool
lq *live_query_mock.MockLiveQuery
}
func (s *integrationEnterpriseTestSuite) SetupSuite() {
s.withDS.SetupSuite("integrationEnterpriseTestSuite")
s.redisPool = redistest.SetupRedis(s.T(), "integration_enterprise", false, false, false)
s.lq = live_query_mock.New(s.T())
config := TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
},
Pool: s.redisPool,
Lq: s.lq,
}
users, server := RunServerForTestsWithDS(s.T(), s.ds, &config)
s.server = server
@ -50,6 +56,11 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
s.token = s.getTestAdminToken()
}
func (s *integrationEnterpriseTestSuite) TearDownTest() {
// reset the mock
s.lq.Mock = mock.Mock{}
}
func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
t := s.T()
@ -65,7 +76,12 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
// updates a team, no secret is provided so it will keep the one generated
// automatically when the team was created.
agentOpts := json.RawMessage(`{"config": {"foo": "bar"}, "overrides": {"platforms": {"darwin": {"foo": "override"}}}}`)
teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName, AgentOptions: &agentOpts}}}
features := fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: false,
AdditionalQueries: ptr.RawMessage(json.RawMessage(`{"foo": "bar"}`)),
}
teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: teamName, AgentOptions: &agentOpts, Features: &features}}}
s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
team, err := s.ds.TeamByName(context.Background(), teamName)
@ -73,6 +89,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
assert.Len(t, team.Secrets, 1)
require.JSONEq(t, string(agentOpts), string(*team.Config.AgentOptions))
require.Equal(t, features, team.Config.Features)
// an activity was created for team spec applied
var listActivities listActivitiesResponse
@ -100,10 +117,13 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() {
team, err = s.ds.TeamByName(context.Background(), "team2")
require.NoError(t, err)
appConfig, err := s.ds.AppConfig(context.Background())
require.NoError(t, err)
defaultOpts := `{"config": {"options": {"logger_plugin": "tls", "pack_delimiter": "/", "logger_tls_period": 10, "distributed_plugin": "tls", "disable_distributed": false, "logger_tls_endpoint": "/api/osquery/log", "distributed_interval": 10, "distributed_tls_max_attempts": 3}, "decorators": {"load": ["SELECT uuid AS host_uuid FROM system_info;", "SELECT hostname AS hostname FROM system_info;"]}}, "overrides": {}}`
assert.Len(t, team.Secrets, 0) // no secret gets created automatically when creating a team via apply spec
require.NotNil(t, team.Config.AgentOptions)
require.JSONEq(t, defaultOpts, string(*team.Config.AgentOptions))
require.Equal(t, appConfig.Features, team.Config.Features)
// an activity was created for the newly created team via the applied spec
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "id", "order_direction", "desc")
@ -1264,3 +1284,73 @@ func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() {
return false
})
}
func (s *integrationEnterpriseTestSuite) TestDistributedReadWithFeatures() {
t := s.T()
// Global config has both features enabled
spec := []byte(`
features:
additional_queries: null
enable_host_users: true
enable_software_inventory: true
`)
s.applyConfig(spec)
// Team config has only additional queries enabled
a := json.RawMessage(`{"time": "SELECT * FROM time"}`)
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 8324,
Name: "team1_" + t.Name(),
Description: "desc team1_" + t.Name(),
Config: fleet.TeamConfig{
Features: fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: false,
AdditionalQueries: &a,
},
},
})
require.NoError(t, err)
// Create a host without a team
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)
s.lq.On("QueriesForHost", host.ID).Return(map[string]string{fmt.Sprintf("%d", host.ID): "select 1 from osquery;"}, nil)
// ensure we can read distributed queries for the host
err = s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
require.NoError(t, err)
// get distributed queries for the host
req := getDistributedQueriesRequest{NodeKey: host.NodeKey}
var dqResp getDistributedQueriesResponse
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
require.Contains(t, dqResp.Queries, "fleet_detail_query_users")
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_macos")
require.NotContains(t, dqResp.Queries, "fleet_additional_query_time")
// add the host to team1
err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})
require.NoError(t, err)
err = s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
require.NoError(t, err)
req = getDistributedQueriesRequest{NodeKey: host.NodeKey}
dqResp = getDistributedQueriesResponse{}
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
require.NotContains(t, dqResp.Queries, "fleet_detail_query_users")
require.NotContains(t, dqResp.Queries, "fleet_detail_query_software_macos")
require.Contains(t, dqResp.Queries, "fleet_additional_query_time")
}

View File

@ -14,6 +14,7 @@ import (
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -501,3 +502,64 @@ func (s *liveQueriesTestSuite) TestOsqueryDistributedRead() {
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusUnauthorized, &errRes)
assert.Contains(t, errRes["error"], "invalid node key")
}
func (s *liveQueriesTestSuite) TestOsqueryDistributedReadWithFeatures() {
t := s.T()
spec := []byte(`
features:
additional_queries:
time: SELECT * FROM time
enable_host_users: true
enable_software_inventory: true
`)
s.applyConfig(spec)
a := json.RawMessage(`{"time": "SELECT * FROM time"}`)
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 42,
Name: "team1",
Description: "desc team1",
Config: fleet.TeamConfig{
Features: fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: false,
AdditionalQueries: &a,
},
},
})
require.NoError(t, err)
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)
s.lq.On("QueriesForHost", host.ID).Return(map[string]string{fmt.Sprintf("%d", host.ID): "select 1 from osquery;"}, nil)
err = s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
require.NoError(t, err)
req := getDistributedQueriesRequest{NodeKey: host.NodeKey}
var dqResp getDistributedQueriesResponse
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
require.Contains(t, dqResp.Queries, "fleet_detail_query_users")
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_macos")
err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})
require.NoError(t, err)
err = s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
require.NoError(t, err)
req = getDistributedQueriesRequest{NodeKey: host.NodeKey}
dqResp = getDistributedQueriesResponse{}
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
require.Contains(t, dqResp.Queries, "fleet_detail_query_users")
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_macos")
}

View File

@ -144,8 +144,13 @@ func (svc *Service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifie
return "", osqueryError{message: "app config load failed: " + err.Error(), nodeInvalid: true}
}
features, err := svc.HostFeatures(ctx, host)
if err != nil {
return "", osqueryError{message: "host features load failed: " + err.Error(), nodeInvalid: true}
}
// Save enrollment details if provided
detailQueries := osquery_utils.GetDetailQueries(appConfig, svc.config)
detailQueries := osquery_utils.GetDetailQueries(svc.config, features)
save := false
if r, ok := hostDetails["os_version"]; ok {
err := detailQueries["os_version"].IngestFunc(ctx, svc.logger, host, []map[string]string{r})
@ -598,15 +603,15 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
return nil, nil, nil
}
config, err := svc.ds.AppConfig(ctx)
features, err := svc.HostFeatures(ctx, host)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "read app config")
return nil, nil, ctxerr.Wrap(ctx, err, "read host features")
}
queries = make(map[string]string)
discovery = make(map[string]string)
detailQueries := osquery_utils.GetDetailQueries(config, svc.config)
detailQueries := osquery_utils.GetDetailQueries(svc.config, features)
for name, query := range detailQueries {
if query.RunsForPlatform(host.Platform) {
queryName := hostDetailQueryPrefix + name
@ -619,13 +624,13 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
}
}
if config.Features.AdditionalQueries == nil {
if features.AdditionalQueries == nil {
// No additional queries set
return queries, discovery, nil
}
var additionalQueries map[string]string
if err := json.Unmarshal(*config.Features.AdditionalQueries, &additionalQueries); err != nil {
if err := json.Unmarshal(*features.AdditionalQueries, &additionalQueries); err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "unmarshal additional queries")
}
@ -684,10 +689,12 @@ func (svc *Service) policyQueriesForHost(ctx context.Context, host *fleet.Host)
// inconsistent, so we use this shim and massage into a consistent
// schema. For example (simplified from actual osqueryd 1.8.2 output):
// {
// "queries": {
// "query_with_no_results": "", // <- Note string instead of array
// "query_with_results": [{"foo":"bar","baz":"bang"}]
// },
//
// "queries": {
// "query_with_no_results": "", // <- Note string instead of array
// "query_with_results": [{"foo":"bar","baz":"bang"}]
// },
//
// "node_key":"IGXCXknWQ1baTa8TZ6rF3kAPZ4\/aTsui"
// }
type submitDistributedQueryResultsRequestShim struct {
@ -948,12 +955,12 @@ func (svc *Service) SubmitDistributedQueryResults(
var noSuchTableRegexp = regexp.MustCompile(`^no such table: \S+$`)
func (svc *Service) directIngestDetailQuery(ctx context.Context, host *fleet.Host, name string, rows []map[string]string, failed bool) (ingested bool, err error) {
config, err := svc.ds.AppConfig(ctx)
features, err := svc.HostFeatures(ctx, host)
if err != nil {
return false, osqueryError{message: "ingest detail query: " + err.Error()}
}
detailQueries := osquery_utils.GetDetailQueries(config, svc.config)
detailQueries := osquery_utils.GetDetailQueries(svc.config, features)
query, ok := detailQueries[name]
if !ok {
return false, osqueryError{message: "unknown detail query " + name}
@ -1073,12 +1080,12 @@ func ingestMembershipQuery(
// ingestDetailQuery takes the results of a detail query and modifies the
// provided fleet.Host appropriately.
func (svc *Service) ingestDetailQuery(ctx context.Context, host *fleet.Host, name string, rows []map[string]string) error {
config, err := svc.ds.AppConfig(ctx)
features, err := svc.HostFeatures(ctx, host)
if err != nil {
return osqueryError{message: "ingest detail query: " + err.Error()}
}
detailQueries := osquery_utils.GetDetailQueries(config, svc.config)
detailQueries := osquery_utils.GetDetailQueries(svc.config, features)
query, ok := detailQueries[name]
if !ok {
return osqueryError{message: "unknown detail query " + name}

View File

@ -6,7 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"reflect"
"sort"
"strconv"
@ -189,7 +189,7 @@ func TestAgentOptionsForHost(t *testing.T) {
// One of these queries is the disk space, only one of the two works in a platform. Similarly, one
// is for operating system.
var expectedDetailQueries = osquery_utils.GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}})
var expectedDetailQueries = osquery_utils.GetDetailQueries(config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, &fleet.Features{EnableHostUsers: true})
func TestEnrollAgent(t *testing.T) {
ds := new(mock.Store)
@ -572,6 +572,132 @@ func TestHostDetailQueries(t *testing.T) {
assert.Equal(t, "select foo", queries[hostAdditionalQueryPrefix+"foobar"])
}
func TestQueriesAndHostFeatures(t *testing.T) {
ds := new(mock.Store)
team1 := fleet.Team{
ID: 1,
Config: fleet.TeamConfig{
Features: fleet.Features{
EnableHostUsers: true,
EnableSoftwareInventory: false,
},
},
}
team2 := fleet.Team{
ID: 2,
Config: fleet.TeamConfig{
Features: fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: true,
},
},
}
host := fleet.Host{
ID: 1,
Platform: "darwin",
NodeKey: "test_key",
Hostname: "test_hostname",
UUID: "test_uuid",
TeamID: nil,
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
Features: fleet.Features{
EnableHostUsers: false,
EnableSoftwareInventory: false,
},
}, nil
}
ds.TeamFeaturesFunc = func(ctx context.Context, id uint) (*fleet.Features, error) {
switch id {
case uint(1):
return &team1.Config.Features, nil
case uint(2):
return &team2.Config.Features, nil
default:
return nil, errors.New("team not found")
}
}
ds.LabelQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
}
ds.PolicyQueriesForHostFunc = func(ctx context.Context, host *fleet.Host) (map[string]string, error) {
return map[string]string{}, nil
}
lq := live_query_mock.New(t)
lq.On("QueriesForHost", uint(1)).Return(map[string]string{}, nil)
lq.On("QueriesForHost", uint(2)).Return(map[string]string{}, nil)
lq.On("QueriesForHost", nil).Return(map[string]string{}, nil)
t.Run("free license", func(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
svc := newTestService(t, ds, nil, lq, &TestServerOpts{License: license})
ctx := hostctx.NewContext(context.Background(), &host)
queries, _, _, err := svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.NotContains(t, queries, "fleet_detail_query_users")
require.NotContains(t, queries, "fleet_detail_query_software_macos")
require.NotContains(t, queries, "fleet_detail_query_software_linux")
require.NotContains(t, queries, "fleet_detail_query_software_windows")
// assign team 1 to host
host.TeamID = &team1.ID
queries, _, _, err = svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.NotContains(t, queries, "fleet_detail_query_users")
require.NotContains(t, queries, "fleet_detail_query_software_macos")
require.NotContains(t, queries, "fleet_detail_query_software_linux")
require.NotContains(t, queries, "fleet_detail_query_software_windows")
// assign team 2 to host
host.TeamID = &team2.ID
queries, _, _, err = svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.NotContains(t, queries, "fleet_detail_query_users")
require.NotContains(t, queries, "fleet_detail_query_software_macos")
require.NotContains(t, queries, "fleet_detail_query_software_linux")
require.NotContains(t, queries, "fleet_detail_query_software_windows")
})
t.Run("premium license", func(t *testing.T) {
license := &fleet.LicenseInfo{Tier: fleet.TierPremium}
svc := newTestService(t, ds, nil, lq, &TestServerOpts{License: license})
host.TeamID = nil
ctx := hostctx.NewContext(context.Background(), &host)
queries, _, _, err := svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.NotContains(t, queries, "fleet_detail_query_users")
require.NotContains(t, queries, "fleet_detail_query_software_macos")
require.NotContains(t, queries, "fleet_detail_query_software_linux")
require.NotContains(t, queries, "fleet_detail_query_software_windows")
// assign team 1 to host
host.TeamID = &team1.ID
queries, _, _, err = svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.Contains(t, queries, "fleet_detail_query_users")
require.NotContains(t, queries, "fleet_detail_query_software_macos")
require.NotContains(t, queries, "fleet_detail_query_software_linux")
require.NotContains(t, queries, "fleet_detail_query_software_windows")
// assign team 2 to host
host.TeamID = &team2.ID
queries, _, _, err = svc.GetDistributedQueries(ctx)
require.NoError(t, err)
require.NotContains(t, queries, "fleet_detail_query_users")
require.Contains(t, queries, "fleet_detail_query_software_macos")
})
}
func TestGetDistributedQueriesMissingHost(t *testing.T) {
svc := newTestService(t, &mock.Store{}, nil, nil)
@ -2682,7 +2808,7 @@ func TestLiveQueriesFailing(t *testing.T) {
require.Equal(t, len(expectedDetailQueries)-2, len(queries), distQueriesMapKeys(queries))
verifyDiscovery(t, queries, discovery)
logs, err := ioutil.ReadAll(buf)
logs, err := io.ReadAll(buf)
require.NoError(t, err)
require.Contains(t, string(logs), "level=error")
require.Contains(t, string(logs), "failed to get queries for host")

View File

@ -1061,7 +1061,7 @@ func directIngestMunkiInfo(ctx context.Context, logger log.Logger, host *fleet.H
return ds.SetOrUpdateMunkiInfo(ctx, host.ID, rows[0]["version"], errList, warnList)
}
func GetDetailQueries(ac *fleet.AppConfig, fleetConfig config.FleetConfig) map[string]DetailQuery {
func GetDetailQueries(fleetConfig config.FleetConfig, features *fleet.Features) map[string]DetailQuery {
generatedMap := make(map[string]DetailQuery)
for key, query := range hostDetailQueries {
generatedMap[key] = query
@ -1070,13 +1070,13 @@ func GetDetailQueries(ac *fleet.AppConfig, fleetConfig config.FleetConfig) map[s
generatedMap[key] = query
}
if ac != nil && ac.Features.EnableSoftwareInventory {
if features != nil && features.EnableSoftwareInventory {
generatedMap["software_macos"] = softwareMacOS
generatedMap["software_linux"] = softwareLinux
generatedMap["software_windows"] = softwareWindows
}
if ac != nil && ac.Features.EnableHostUsers {
if features != nil && features.EnableHostUsers {
generatedMap["users"] = usersQuery
}

View File

@ -24,7 +24,7 @@ func TestDetailQueryNetworkInterfaces(t *testing.T) {
var initialHost fleet.Host
host := initialHost
ingest := GetDetailQueries(nil, config.FleetConfig{})["network_interface"].IngestFunc
ingest := GetDetailQueries(config.FleetConfig{}, nil)["network_interface"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@ -118,7 +118,7 @@ func TestDetailQueryScheduledQueryStats(t *testing.T) {
return nil
}
ingest := GetDetailQueries(nil, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}})["scheduled_query_stats"].DirectTaskIngestFunc
ingest := GetDetailQueries(config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil)["scheduled_query_stats"].DirectTaskIngestFunc
ctx := context.Background()
assert.NoError(t, ingest(ctx, log.NewNopLogger(), &host, task, nil, false))
@ -296,7 +296,7 @@ func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []st
}
func TestGetDetailQueries(t *testing.T) {
queriesNoConfig := GetDetailQueries(nil, config.FleetConfig{})
queriesNoConfig := GetDetailQueries(config.FleetConfig{}, nil)
require.Len(t, queriesNoConfig, 16)
baseQueries := []string{
@ -319,14 +319,14 @@ func TestGetDetailQueries(t *testing.T) {
}
sortedKeysCompare(t, queriesNoConfig, baseQueries)
queriesWithoutWinOSVuln := GetDetailQueries(nil, config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}})
queriesWithoutWinOSVuln := GetDetailQueries(config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil)
require.Len(t, queriesWithoutWinOSVuln, 15)
queriesWithUsers := GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}})
queriesWithUsers := GetDetailQueries(config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, &fleet.Features{EnableHostUsers: true})
require.Len(t, queriesWithUsers, 18)
sortedKeysCompare(t, queriesWithUsers, append(baseQueries, "users", "scheduled_query_stats"))
queriesWithUsersAndSoftware := GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}}, config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}})
queriesWithUsersAndSoftware := GetDetailQueries(config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true})
require.Len(t, queriesWithUsersAndSoftware, 21)
sortedKeysCompare(t, queriesWithUsersAndSoftware,
append(baseQueries, "users", "software_macos", "software_linux", "software_windows", "scheduled_query_stats"))
@ -336,7 +336,7 @@ func TestDetailQueriesOSVersion(t *testing.T) {
var initialHost fleet.Host
host := initialHost
ingest := GetDetailQueries(nil, config.FleetConfig{})["os_version"].IngestFunc
ingest := GetDetailQueries(config.FleetConfig{}, nil)["os_version"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@ -651,15 +651,15 @@ func TestDirectIngestOSUnixLike(t *testing.T) {
}
func TestDangerousReplaceQuery(t *testing.T) {
queries := GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{})
queries := GetDetailQueries(config.FleetConfig{}, &fleet.Features{EnableHostUsers: true})
originalQuery := queries["users"].Query
t.Setenv("FLEET_DANGEROUS_REPLACE_USERS", "select * from blah")
queries = GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{})
queries = GetDetailQueries(config.FleetConfig{}, &fleet.Features{EnableHostUsers: true})
assert.NotEqual(t, originalQuery, queries["users"].Query)
require.NoError(t, os.Unsetenv("FLEET_DANGEROUS_REPLACE_USERS"))
queries = GetDetailQueries(&fleet.AppConfig{Features: fleet.Features{EnableHostUsers: true}}, config.FleetConfig{})
queries = GetDetailQueries(config.FleetConfig{}, &fleet.Features{EnableHostUsers: true})
assert.Equal(t, originalQuery, queries["users"].Query)
}

View File

@ -48,12 +48,18 @@ type Service struct {
jitterH map[time.Duration]*jitterHashTable
geoIP fleet.GeoIP
*fleet.EnterpriseOverrides
}
func (s *Service) LookupGeoIP(ctx context.Context, ip string) *fleet.GeoLocation {
return s.geoIP.Lookup(ctx, ip)
}
func (s *Service) SetEnterpriseOverrides(overrides fleet.EnterpriseOverrides) {
s.EnterpriseOverrides = &overrides
}
// NewService creates a new service from the config struct
func NewService(
ctx context.Context,

View File

@ -3,7 +3,7 @@ package service
import (
"context"
"encoding/json"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"os"
@ -286,7 +286,7 @@ func testUnrecognizedPluginConfig() config.FleetConfig {
}
func assertBodyContains(t *testing.T, resp *http.Response, expected string) {
bodyBytes, err := ioutil.ReadAll(resp.Body)
bodyBytes, err := io.ReadAll(resp.Body)
require.Nil(t, err)
bodyString := string(bodyBytes)
assert.Contains(t, bodyString, expected)