diff --git a/changes/issue-7312-features-config b/changes/issue-7312-features-config index 3289b22b0..525ffa9ce 100644 --- a/changes/issue-7312-features-config +++ b/changes/issue-7312-features-config @@ -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. diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index ed6bbaa3b..c68758234 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -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 diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index bb4283ee9..7501c3308 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -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": { diff --git a/docs/Using-Fleet/configuration-files/README.md b/docs/Using-Fleet/configuration-files/README.md index b48206636..2d1782260 100644 --- a/docs/Using-Fleet/configuration-files/README.md +++ b/docs/Using-Fleet/configuration-files/README.md @@ -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: diff --git a/ee/server/service/appconfig.go b/ee/server/service/appconfig.go new file mode 100644 index 000000000..872142bad --- /dev/null +++ b/ee/server/service/appconfig.go @@ -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 +} diff --git a/ee/server/service/service.go b/ee/server/service/service.go index d437616f6..452386a55 100644 --- a/ee/server/service/service.go +++ b/ee/server/service/service.go @@ -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 } diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 6529f9ea3..a5f40b783 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -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 +} diff --git a/server/datastore/cached_mysql/cached_mysql.go b/server/datastore/cached_mysql/cached_mysql.go index f354ff7e7..4d42366ce 100644 --- a/server/datastore/cached_mysql/cached_mysql.go +++ b/server/datastore/cached_mysql/cached_mysql.go @@ -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 } diff --git a/server/datastore/cached_mysql/cached_mysql_test.go b/server/datastore/cached_mysql/cached_mysql_test.go index 97cdd6bb4..391e8f99c 100644 --- a/server/datastore/cached_mysql/cached_mysql_test.go +++ b/server/datastore/cached_mysql/cached_mysql_test.go @@ -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) +} diff --git a/server/datastore/mysql/teams.go b/server/datastore/mysql/teams.go index b04ab1f86..8f27f7616 100644 --- a/server/datastore/mysql/teams.go +++ b/server/datastore/mysql/teams.go @@ -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 { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 125fafbfc..cd2c67a73 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -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' diff --git a/server/fleet/service.go b/server/fleet/service.go index abacb49c7..5f70c1189 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -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. diff --git a/server/fleet/teams.go b/server/fleet/teams.go index c86f13bb0..a18afe97b 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -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"` } diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index b159e228c..bb3f77472 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -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) diff --git a/server/service/appconfig.go b/server/service/appconfig.go index d0b291783..c643e9d8b 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -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 +} diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d20f01ba6..9441ad4b9 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -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") +} diff --git a/server/service/integration_live_queries_test.go b/server/service/integration_live_queries_test.go index ab64e911f..05b3a475c 100644 --- a/server/service/integration_live_queries_test.go +++ b/server/service/integration_live_queries_test.go @@ -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") +} diff --git a/server/service/osquery.go b/server/service/osquery.go index 8fb4ff678..2268211fa 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -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} diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 9a38d04ef..69e079456 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -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") diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 4b9e0f403..a20860ca1 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -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 } diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 5e132dc8e..3613728eb 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -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) } diff --git a/server/service/service.go b/server/service/service.go index 503b5bcf2..47ccf66c8 100644 --- a/server/service/service.go +++ b/server/service/service.go @@ -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, diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 8ece0da6e..279083975 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -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)