mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
enable controlled rollout of features by teams (#7408)
This commit is contained in:
parent
a2dc154803
commit
eeefe2fab9
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
29
ee/server/service/appconfig.go
Normal file
29
ee/server/service/appconfig.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user