Update agent options storage for teams (#754)

- Allow agent options to be set on per-team basis.
- Move global agent options into app configs.
- Update logic for calculating agent options for hosts.
- Updates to relevant testing.
This commit is contained in:
Zach Wasserman 2021-05-11 18:15:16 -07:00 committed by GitHub
parent 40f2452e46
commit b1a98a6e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 302 additions and 786 deletions

View File

@ -27,7 +27,6 @@ type specGroup struct {
Queries []*kolide.QuerySpec
Packs []*kolide.PackSpec
Labels []*kolide.LabelSpec
Options *kolide.OptionsSpec
AppConfig *kolide.AppConfigPayload
EnrollSecret *kolide.EnrollSecretSpec
}
@ -73,17 +72,6 @@ func specGroupFromBytes(b []byte) (*specGroup, error) {
}
specs.Labels = append(specs.Labels, labelSpec)
case kolide.OptionsKind:
if specs.Options != nil {
return nil, errors.New("options defined twice in the same file")
}
var optionSpec *kolide.OptionsSpec
if err := yaml.Unmarshal(s.Spec, &optionSpec); err != nil {
return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
}
specs.Options = optionSpec
case kolide.AppConfigKind:
if specs.AppConfig != nil {
return nil, errors.New("config defined twice in the same file")
@ -175,13 +163,6 @@ func applyCommand() *cli.Command {
fmt.Printf("[+] applied %d packs\n", len(specs.Packs))
}
if specs.Options != nil {
if err := fleet.ApplyOptions(specs.Options); err != nil {
return errors.Wrap(err, "applying options")
}
fmt.Printf("[+] applied options\n")
}
if specs.AppConfig != nil {
if err := fleet.ApplyAppConfig(specs.AppConfig); err != nil {
return errors.Wrap(err, "applying fleet config")

View File

@ -120,24 +120,6 @@ func printPack(c *cli.Context, pack *kolide.PackSpec) error {
return err
}
func printOption(c *cli.Context, option *kolide.OptionsSpec) error {
spec := specGeneric{
Kind: kolide.OptionsKind,
Version: kolide.ApiVersion,
Spec: option,
}
var err error
if c.Bool(jsonFlagName) {
err = printJSON(spec)
} else {
err = printYaml(spec)
}
return err
}
func printSecret(c *cli.Context, secret *kolide.EnrollSecretSpec) error {
spec := specGeneric{
Kind: kolide.EnrollSecretKind,
@ -209,7 +191,6 @@ func getCommand() *cli.Command {
getQueriesCommand(),
getPacksCommand(),
getLabelsCommand(),
getOptionsCommand(),
getHostsCommand(),
getEnrollSecretCommand(),
getAppConfigCommand(),
@ -484,38 +465,6 @@ func getLabelsCommand() *cli.Command {
}
}
func getOptionsCommand() *cli.Command {
return &cli.Command{
Name: "options",
Usage: "Retrieve the osquery configuration",
Flags: []cli.Flag{
jsonFlag(),
yamlFlag(),
configFlag(),
contextFlag(),
debugFlag(),
},
Action: func(c *cli.Context) error {
fleet, err := clientFromCLI(c)
if err != nil {
return err
}
options, err := fleet.GetOptions()
if err != nil {
return err
}
err = printOption(c, options)
if err != nil {
return err
}
return nil
},
}
}
func getEnrollSecretCommand() *cli.Command {
return &cli.Command{
Name: "enroll_secret",

View File

@ -79,9 +79,6 @@ var TestFunctions = [...]func(*testing.T, kolide.Datastore){
testCountHostsInTargets,
testHostStatus,
testHostIDsInTargets,
testApplyOsqueryOptions,
testApplyOsqueryOptionsNoOverrides,
testOsqueryOptionsForHost,
testApplyQueries,
testApplyPackSpecRoundtrip,
testApplyPackSpecMissingQueries,

View File

@ -1,95 +0,0 @@
package datastore
import (
"encoding/json"
"testing"
"github.com/fleetdm/fleet/server/kolide"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testApplyOsqueryOptions(t *testing.T, ds kolide.Datastore) {
expectedOpts := &kolide.OptionsSpec{
Config: json.RawMessage(`{"foo": "bar"}`),
Overrides: kolide.OptionsOverrides{
Platforms: map[string]json.RawMessage{
"darwin": json.RawMessage(`{"froob": "ling"}`),
},
},
}
err := ds.ApplyOptions(expectedOpts)
require.Nil(t, err)
retrievedOpts, err := ds.GetOptions()
require.Nil(t, err)
assert.Equal(t, expectedOpts, retrievedOpts)
// Re-apply and verify everything has been replaced.
expectedOpts = &kolide.OptionsSpec{
Config: json.RawMessage(`{"blue": "smurf"}`),
Overrides: kolide.OptionsOverrides{
Platforms: map[string]json.RawMessage{
"linux": json.RawMessage(`{"transitive": "nightfall"}`),
},
},
}
err = ds.ApplyOptions(expectedOpts)
require.Nil(t, err)
retrievedOpts, err = ds.GetOptions()
require.Nil(t, err)
assert.Equal(t, expectedOpts, retrievedOpts)
}
func testApplyOsqueryOptionsNoOverrides(t *testing.T, ds kolide.Datastore) {
expectedOpts := &kolide.OptionsSpec{
Config: json.RawMessage(`{}`),
}
err := ds.ApplyOptions(expectedOpts)
require.Nil(t, err)
retrievedOpts, err := ds.GetOptions()
require.Nil(t, err)
assert.Equal(t, expectedOpts.Config, retrievedOpts.Config)
assert.Empty(t, retrievedOpts.Overrides.Platforms)
}
func testOsqueryOptionsForHost(t *testing.T, ds kolide.Datastore) {
defaultOpts := json.RawMessage(`{"foo": "bar"}`)
darwinOpts := json.RawMessage(`{"darwin": "macintosh"}`)
linuxOpts := json.RawMessage(`{"linux": "FOSS"}`)
expectedOpts := &kolide.OptionsSpec{
Config: defaultOpts,
Overrides: kolide.OptionsOverrides{
Platforms: map[string]json.RawMessage{
"darwin": darwinOpts,
"linux": linuxOpts,
},
},
}
err := ds.ApplyOptions(expectedOpts)
require.Nil(t, err)
var testCases = []struct {
host kolide.Host
expectedOpts json.RawMessage
}{
{kolide.Host{Platform: "windows"}, defaultOpts},
{kolide.Host{Platform: "linux"}, linuxOpts},
{kolide.Host{Platform: "darwin"}, darwinOpts},
{kolide.Host{Platform: "some_other_platform"}, defaultOpts},
}
for _, tt := range testCases {
t.Run("", func(t *testing.T) {
opts, err := ds.OptionsForPlatform(tt.host.Platform)
require.Nil(t, err)
assert.Equal(t, tt.expectedOpts, opts)
})
}
}

View File

@ -8,7 +8,6 @@ import (
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
@ -153,7 +152,7 @@ func migrateOptions(tx *sql.Tx) error {
override_type, override_identifier, options
) VALUES (?, ?, ?)
`
if _, err = txx.Exec(query, kolide.OptionOverrideTypeDefault, "", string(confJSON)); err != nil {
if _, err = txx.Exec(query, 0, "", string(confJSON)); err != nil {
return errors.Wrap(err, "saving converted options")
}

View File

@ -0,0 +1,85 @@
package tables
import (
"database/sql"
"encoding/json"
"github.com/fleetdm/fleet/server/kolide"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20210510111225, Down_20210510111225)
}
func Up_20210510111225(tx *sql.Tx) error {
existingOptions, err := copyOptions(tx)
if err != nil {
return errors.Wrap(err, "get existing options")
}
sql := `
ALTER TABLE app_configs
ADD COLUMN agent_options JSON
`
if _, err := tx.Exec(sql); err != nil {
return errors.Wrap(err, "add column agent_options")
}
sql = `UPDATE app_configs SET agent_options = ?`
if _, err := tx.Exec(sql, existingOptions); err != nil {
return errors.Wrap(err, "insert existing options")
}
return nil
}
// Below code copied and adapted from osquery options code removed in this commit.
func copyOptions(tx *sql.Tx) (json.RawMessage, error) {
// Migrate pre teams osquery options to the new osquery option storage in app config.
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
var rows []optionsRow
if err := txx.Select(&rows, "SELECT * FROM osquery_options"); err != nil {
return nil, errors.Wrap(err, "selecting options")
}
opt := &kolide.AgentOptions{
Overrides: kolide.AgentOptionsOverrides{
Platforms: make(map[string]json.RawMessage),
},
}
for _, row := range rows {
switch row.OverrideType {
case 0: // was kolide.OptionOverrideTypeDefault
opt.Config = json.RawMessage(row.Options)
case 1: // was kolide.OptionOverrideTypePlatform
opt.Overrides.Platforms[row.OverrideIdentifier] = json.RawMessage(row.Options)
default:
return nil, errors.Errorf("unknown override type: %d", row.OverrideType)
}
}
jsonVal, err := json.Marshal(opt)
if err != nil {
return nil, errors.Wrap(err, "marshal options")
}
return jsonVal, nil
}
type optionsRow struct {
ID int `db:"id"`
OverrideType int `db:"override_type"`
OverrideIdentifier string `db:"override_identifier"`
Options string `db:"options"`
}
func Down_20210510111225(tx *sql.Tx) error {
return nil
}

View File

@ -1,132 +0,0 @@
package mysql
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/go-kit/kit/log/level"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
type optionsRow struct {
ID int `db:"id"`
OverrideType kolide.OptionOverrideType `db:"override_type"`
OverrideIdentifier string `db:"override_identifier"`
Options string `db:"options"`
}
func (d *Datastore) ApplyOptions(spec *kolide.OptionsSpec) (err error) {
tx, err := d.db.Begin()
if err != nil {
return errors.Wrap(err, "begin ApplyOptions transaction")
}
defer func() {
if err != nil {
rbErr := tx.Rollback()
// It seems possible that there might be a case in
// which the error we are dealing with here was thrown
// by the call to tx.Commit(), and the docs suggest
// this call would then result in sql.ErrTxDone.
if rbErr != nil && rbErr != sql.ErrTxDone {
panic(fmt.Sprintf("got err '%s' rolling back after err '%s'", rbErr, err))
}
}
}()
// Clear all the existing options
_, err = tx.Exec("DELETE FROM osquery_options")
if err != nil {
return errors.Wrap(err, "delete existing options")
}
// Save new options
sql := `
INSERT INTO osquery_options (
override_type, override_identifier, options
) VALUES (?, ?, ?)
`
// Default options
_, err = tx.Exec(sql, kolide.OptionOverrideTypeDefault, "", string(spec.Config))
if err != nil {
return errors.Wrap(err, "saving default config")
}
// Platform overrides
for platform, opts := range spec.Overrides.Platforms {
_, err = tx.Exec(sql, kolide.OptionOverrideTypePlatform, platform, string(opts))
if err != nil {
return errors.Wrapf(err, "saving %s platform config", platform)
}
}
// Success!
err = tx.Commit()
if err != nil {
return errors.Wrap(err, "commit ApplyOptions transaction")
}
return nil
}
func (d *Datastore) GetOptions() (*kolide.OptionsSpec, error) {
var rows []optionsRow
if err := d.db.Select(&rows, "SELECT * FROM osquery_options"); err != nil {
return nil, errors.Wrap(err, "selecting options")
}
spec := &kolide.OptionsSpec{
Overrides: kolide.OptionsOverrides{
Platforms: make(map[string]json.RawMessage),
},
}
for _, row := range rows {
switch row.OverrideType {
case kolide.OptionOverrideTypeDefault:
spec.Config = json.RawMessage(row.Options)
case kolide.OptionOverrideTypePlatform:
spec.Overrides.Platforms[row.OverrideIdentifier] = json.RawMessage(row.Options)
default:
level.Info(d.logger).Log(
"err", "ignoring unkown override type",
"type", row.OverrideType,
)
}
}
return spec, nil
}
func (d *Datastore) OptionsForPlatform(platform string) (json.RawMessage, error) {
// SQL uses a custom ordering function to return the single correct
// config with the highest precedence override (the FIELD function
// defines this ordering). If there is no override, it returns the
// default.
sql := `
SELECT * FROM osquery_options
WHERE override_type = ? OR
(override_type = ? AND override_identifier = ?)
ORDER BY FIELD(override_type, ?, ?)
LIMIT 1
`
var row optionsRow
err := d.db.Get(
&row, sql,
kolide.OptionOverrideTypeDefault,
kolide.OptionOverrideTypePlatform, platform,
// Order of the following arguments defines precedence of
// overrides.
kolide.OptionOverrideTypePlatform, kolide.OptionOverrideTypeDefault,
)
if err != nil {
return nil, errors.Wrapf(err, "retrieving osquery options for platform '%s'", platform)
}
return json.RawMessage(row.Options), nil
}

View File

@ -0,0 +1,36 @@
package kolide
import (
"context"
"encoding/json"
)
type AgentOptionsService interface {
// AgentOptionsForHost gets the agent options for the provided host.
//
// The host information should be used for filtering based on team,
// platform, etc.
AgentOptionsForHost(ctx context.Context, host *Host) (json.RawMessage, error)
}
type AgentOptions struct {
// Config is the base config options.
Config json.RawMessage `json:"config"`
// Overrides includes any platform-based overrides.
Overrides AgentOptionsOverrides `json:"overrides,omitempty"`
}
type AgentOptionsOverrides struct {
// Platforms is a map from platform name to the config override.
Platforms map[string]json.RawMessage `json:"platforms,omitempty"`
}
func (o *AgentOptions) ForPlatform(platform string) json.RawMessage {
// Return matching platform override if available.
if opt, ok := o.Overrides.Platforms[platform]; ok {
return opt
}
// Otherwise return base config for team.
return o.Config
}

View File

@ -167,6 +167,9 @@ type AppConfig struct {
// AdditionalQueries is the set of additional queries that should be run
// when collecting details from hosts.
AdditionalQueries *json.RawMessage `db:"additional_queries"`
// AgentOptions is the global agent options, including overrides.
AgentOptions json.RawMessage `db:"agent_options"`
}
const (

View File

@ -14,7 +14,6 @@ type Datastore interface {
AppConfigStore
InviteStore
ScheduledQueryStore
OsqueryOptionsStore
CarveStore
TeamStore
SoftwareStore

View File

@ -1,50 +0,0 @@
package kolide
import (
"context"
"encoding/json"
)
type OsqueryOptionsStore interface {
ApplyOptions(options *OptionsSpec) error
GetOptions() (*OptionsSpec, error)
OptionsForPlatform(platform string) (json.RawMessage, error)
}
type OsqueryOptionsService interface {
ApplyOptionsSpec(ctx context.Context, spec *OptionsSpec) error
GetOptionsSpec(ctx context.Context) (*OptionsSpec, error)
}
type OptionsObject struct {
ObjectMetadata
Spec OptionsSpec `json:"spec"`
}
type OptionsSpec struct {
Config json.RawMessage `json:"config"`
Overrides OptionsOverrides `json:"overrides,omitempty"`
}
type OptionsOverrides struct {
Platforms map[string]json.RawMessage `json:"platforms,omitempty"`
}
const (
OptionsKind = "options"
)
// OptionOverrideType is used to designate which override type a given set of
// options is used for. Currently the only supported override type is by
// platform.
type OptionOverrideType int
const (
// OptionOverrideTypeDefault indicates that this is the default config
// (provided to hosts when there is no override set for them).
OptionOverrideTypeDefault OptionOverrideType = iota
// OptionOverrideTypePlatform indicates that this is a
// platform-specific config override (with precedence over the default
// config).
OptionOverrideTypePlatform
)

View File

@ -1,119 +0,0 @@
package kolide
import (
"testing"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUnmarshalYaml(t *testing.T) {
y := []byte(`
apiVersion: k8s.kolide.com/v1alpha1
kind: OsqueryOptions
spec:
config:
options:
distributed_interval: 3
distributed_tls_max_attempts: 3
logger_plugin: tls
logger_tls_endpoint: /api/v1/osquery/log
logger_tls_period: 10
overrides:
platforms:
darwin:
options:
distributed_interval: 10
distributed_tls_max_attempts: 10
logger_plugin: tls
logger_tls_endpoint: /api/v1/osquery/log
logger_tls_period: 300
disable_tables: chrome_extensions
docker_socket: /var/run/docker.sock
file_paths:
users:
- /Users/%/Library/%%
- /Users/%/Documents/%%
etc:
- /etc/%%
linux:
options:
distributed_interval: 10
distributed_tls_max_attempts: 3
logger_plugin: tls
logger_tls_endpoint: /api/v1/osquery/log
logger_tls_period: 60
schedule_timeout: 60
docker_socket: /etc/run/docker.sock
frobulations:
- fire
- ice
`)
expectedConfig := `{
"options":{
"distributed_interval":3,
"distributed_tls_max_attempts":3,
"logger_plugin":"tls",
"logger_tls_endpoint":"/api/v1/osquery/log",
"logger_tls_period":10
}
}`
expectedDarwin := `{
"options":{
"disable_tables":"chrome_extensions",
"distributed_interval":10,
"distributed_tls_max_attempts":10,
"docker_socket":"/var/run/docker.sock",
"logger_plugin":"tls",
"logger_tls_endpoint":"/api/v1/osquery/log",
"logger_tls_period":300
},
"file_paths":{
"etc":[
"/etc/%%"
],
"users":[
"/Users/%/Library/%%",
"/Users/%/Documents/%%"
]
}
}`
expectedLinux := `{
"options":{
"distributed_interval":10,
"distributed_tls_max_attempts":3,
"docker_socket":"/etc/run/docker.sock",
"logger_plugin":"tls",
"logger_tls_endpoint":"/api/v1/osquery/log",
"logger_tls_period":60,
"schedule_timeout":60
},
"frobulations": [
"fire",
"ice"
]
}`
var foo OptionsObject
err := yaml.Unmarshal(y, &foo)
require.Nil(t, err)
assert.JSONEq(t, expectedConfig, string(foo.Spec.Config))
platformOverrides := foo.Spec.Overrides.Platforms
assert.Len(t, platformOverrides, 2)
if assert.Contains(t, platformOverrides, "darwin") {
assert.JSONEq(t, expectedDarwin, string(platformOverrides["darwin"]))
}
if assert.Contains(t, platformOverrides, "linux") {
assert.JSONEq(t, expectedLinux, string(platformOverrides["linux"]))
}
}

View File

@ -9,7 +9,7 @@ type Service interface {
QueryService
CampaignService
OsqueryService
OsqueryOptionsService
AgentOptionsService
HostService
AppConfigService
InviteService

View File

@ -9,14 +9,14 @@ import (
type TeamStore interface {
// NewTeam creates a new Team object in the store.
NewTeam(team *Team) (*Team, error)
// SaveTeam saves any changes to the team.
SaveTeam(team *Team) (*Team, error)
// Team retrieves the Team by ID.
Team(tid uint) (*Team, error)
// Team deletes the Team by ID.
DeleteTeam(tid uint) error
// TeamByName retrieves the Team by Name.
TeamByName(name string) (*Team, error)
// SaveTeam saves any changes to the team.
SaveTeam(team *Team) (*Team, error)
// ListTeams lists teams with the ordering and filters in the provided
// options.
ListTeams(opt ListOptions) ([]*Team, error)

View File

@ -21,12 +21,11 @@ var _ kolide.Datastore = (*Store)(nil)
type Store struct {
kolide.PasswordResetStore
kolide.TeamStore
TeamStore
TargetStore
SessionStore
CampaignStore
ScheduledQueryStore
OsqueryOptionsStore
AppConfigStore
HostStore
InviteStore

View File

@ -1,43 +0,0 @@
// Automatically generated by mockimpl. DO NOT EDIT!
package mock
import (
"encoding/json"
"github.com/fleetdm/fleet/server/kolide"
)
var _ kolide.OsqueryOptionsStore = (*OsqueryOptionsStore)(nil)
type ApplyOptionsFunc func(options *kolide.OptionsSpec) error
type GetOptionsFunc func() (*kolide.OptionsSpec, error)
type OptionsForPlatformFunc func(platform string) (json.RawMessage, error)
type OsqueryOptionsStore struct {
ApplyOptionsFunc ApplyOptionsFunc
ApplyOptionsFuncInvoked bool
GetOptionsFunc GetOptionsFunc
GetOptionsFuncInvoked bool
OptionsForPlatformFunc OptionsForPlatformFunc
OptionsForPlatformFuncInvoked bool
}
func (s *OsqueryOptionsStore) ApplyOptions(options *kolide.OptionsSpec) error {
s.ApplyOptionsFuncInvoked = true
return s.ApplyOptionsFunc(options)
}
func (s *OsqueryOptionsStore) GetOptions() (*kolide.OptionsSpec, error) {
s.GetOptionsFuncInvoked = true
return s.GetOptionsFunc()
}
func (s *OsqueryOptionsStore) OptionsForPlatform(platform string) (json.RawMessage, error) {
s.OptionsForPlatformFuncInvoked = true
return s.OptionsForPlatformFunc(platform)
}

View File

@ -0,0 +1,71 @@
// Automatically generated by mockimpl. DO NOT EDIT!
package mock
import (
"github.com/fleetdm/fleet/server/kolide"
)
var _ kolide.TeamStore = (*TeamStore)(nil)
type NewTeamFunc func(team *kolide.Team) (*kolide.Team, error)
type SaveTeamFunc func(team *kolide.Team) (*kolide.Team, error)
type DeleteTeamFunc func(tid uint) error
type TeamFunc func(id uint) (*kolide.Team, error)
type TeamByNameFunc func(name string) (*kolide.Team, error)
type ListTeamsFunc func(opt kolide.ListOptions) ([]*kolide.Team, error)
type TeamStore struct {
NewTeamFunc NewTeamFunc
NewTeamFuncInvoked bool
SaveTeamFunc SaveTeamFunc
SaveTeamFuncInvoked bool
DeleteTeamFunc DeleteTeamFunc
DeleteTeamFuncInvoked bool
TeamFunc TeamFunc
TeamFuncInvoked bool
TeamByNameFunc TeamByNameFunc
TeamByNameFuncInvoked bool
ListTeamsFunc ListTeamsFunc
ListTeamsFuncInvoked bool
}
func (s *TeamStore) NewTeam(team *kolide.Team) (*kolide.Team, error) {
s.NewTeamFuncInvoked = true
return s.NewTeamFunc(team)
}
func (s *TeamStore) SaveTeam(team *kolide.Team) (*kolide.Team, error) {
s.SaveTeamFuncInvoked = true
return s.SaveTeamFunc(team)
}
func (s *TeamStore) DeleteTeam(tid uint) error {
s.DeleteTeamFuncInvoked = true
return s.DeleteTeamFunc(tid)
}
func (s *TeamStore) Team(id uint) (*kolide.Team, error) {
s.TeamFuncInvoked = true
return s.TeamFunc(id)
}
func (s *TeamStore) TeamByName(identifier string) (*kolide.Team, error) {
s.TeamByNameFuncInvoked = true
return s.TeamByNameFunc(identifier)
}
func (s *TeamStore) ListTeams(opt kolide.ListOptions) ([]*kolide.Team, error) {
s.ListTeamsFuncInvoked = true
return s.ListTeamsFunc(opt)
}

View File

@ -1,73 +0,0 @@
package service
import (
"encoding/json"
"net/http"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
// ApplyOptions sends the osquery options to be applied to the Fleet instance.
func (c *Client) ApplyOptions(spec *kolide.OptionsSpec) error {
req := applyOsqueryOptionsSpecRequest{Spec: spec}
response, err := c.AuthenticatedDo("POST", "/api/v1/fleet/spec/osquery_options", "", req)
if err != nil {
return errors.Wrap(err, "POST /api/v1/fleet/spec/osquery_options")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return errors.Errorf(
"apply options received status %d %s",
response.StatusCode,
extractServerErrorText(response.Body),
)
}
var responseBody applyOsqueryOptionsSpecResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return errors.Wrap(err, "decode apply options spec response")
}
if responseBody.Err != nil {
return errors.Errorf("apply options spec: %s", responseBody.Err)
}
return nil
}
// GetOptions retrieves the configured osquery options.
func (c *Client) GetOptions() (*kolide.OptionsSpec, error) {
verb, path := "GET", "/api/v1/fleet/spec/osquery_options"
response, err := c.AuthenticatedDo(verb, path, "", nil)
if err != nil {
return nil, errors.Wrap(err, verb+" "+path)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusNotFound:
return nil, notFoundErr{}
}
if response.StatusCode != http.StatusOK {
return nil, errors.Errorf(
"get options received status %d %s",
response.StatusCode,
extractServerErrorText(response.Body),
)
}
var responseBody getOsqueryOptionsSpecResponse
err = json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
return nil, errors.Wrap(err, "decode get options spec response")
}
if responseBody.Err != nil {
return nil, errors.Errorf("get options spec: %s", responseBody.Err)
}
return responseBody.Spec, nil
}

View File

@ -1,54 +0,0 @@
package service
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/fleetdm/fleet/server/kolide"
)
////////////////////////////////////////////////////////////////////////////////
// Apply Options Spec
////////////////////////////////////////////////////////////////////////////////
type applyOsqueryOptionsSpecRequest struct {
Spec *kolide.OptionsSpec `json:"spec"`
}
type applyOsqueryOptionsSpecResponse struct {
Err error `json:"error,omitempty"`
}
func (r applyOsqueryOptionsSpecResponse) error() error { return r.Err }
func makeApplyOsqueryOptionsSpecEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(applyOsqueryOptionsSpecRequest)
err := svc.ApplyOptionsSpec(ctx, req.Spec)
if err != nil {
return applyOsqueryOptionsSpecResponse{Err: err}, nil
}
return applyOsqueryOptionsSpecResponse{}, nil
}
}
////////////////////////////////////////////////////////////////////////////////
// Get Options Spec
////////////////////////////////////////////////////////////////////////////////
type getOsqueryOptionsSpecResponse struct {
Spec *kolide.OptionsSpec `json:"spec"`
Err error `json:"error,omitempty"`
}
func (r getOsqueryOptionsSpecResponse) error() error { return r.Err }
func makeGetOsqueryOptionsSpecEndpoint(svc kolide.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
spec, err := svc.GetOptionsSpec(ctx)
if err != nil {
return getOsqueryOptionsSpecResponse{Err: err}, nil
}
return getOsqueryOptionsSpecResponse{Spec: spec}, nil
}
}

View File

@ -97,8 +97,6 @@ type KolideEndpoints struct {
ListHosts endpoint.Endpoint
GetHostSummary endpoint.Endpoint
SearchTargets endpoint.Endpoint
ApplyOsqueryOptionsSpec endpoint.Endpoint
GetOsqueryOptionsSpec endpoint.Endpoint
GetCertificate endpoint.Endpoint
ChangeEmail endpoint.Endpoint
InitiateSSO endpoint.Endpoint
@ -211,8 +209,6 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string, lim
GetLabelSpecs: authenticatedUser(jwtKey, svc, makeGetLabelSpecsEndpoint(svc)),
GetLabelSpec: authenticatedUser(jwtKey, svc, makeGetLabelSpecEndpoint(svc)),
SearchTargets: authenticatedUser(jwtKey, svc, makeSearchTargetsEndpoint(svc)),
ApplyOsqueryOptionsSpec: authenticatedUser(jwtKey, svc, makeApplyOsqueryOptionsSpecEndpoint(svc)),
GetOsqueryOptionsSpec: authenticatedUser(jwtKey, svc, makeGetOsqueryOptionsSpecEndpoint(svc)),
GetCertificate: authenticatedUser(jwtKey, svc, makeCertificateEndpoint(svc)),
ChangeEmail: authenticatedUser(jwtKey, svc, makeChangeEmailEndpoint(svc)),
ListCarves: authenticatedUser(jwtKey, svc, makeListCarvesEndpoint(svc)),
@ -323,8 +319,6 @@ type kolideHandlers struct {
ListHosts http.Handler
GetHostSummary http.Handler
SearchTargets http.Handler
ApplyOsqueryOptionsSpec http.Handler
GetOsqueryOptionsSpec http.Handler
GetCertificate http.Handler
ChangeEmail http.Handler
InitiateSSO http.Handler
@ -426,8 +420,6 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli
ListHosts: newServer(e.ListHosts, decodeListHostsRequest),
GetHostSummary: newServer(e.GetHostSummary, decodeNoParamsRequest),
SearchTargets: newServer(e.SearchTargets, decodeSearchTargetsRequest),
ApplyOsqueryOptionsSpec: newServer(e.ApplyOsqueryOptionsSpec, decodeApplyOsqueryOptionsSpecRequest),
GetOsqueryOptionsSpec: newServer(e.GetOsqueryOptionsSpec, decodeNoParamsRequest),
GetCertificate: newServer(e.GetCertificate, decodeNoParamsRequest),
ChangeEmail: newServer(e.ChangeEmail, decodeChangeEmailRequest),
InitiateSSO: newServer(e.InitiateSSO, decodeInitiateSSORequest),
@ -644,9 +636,6 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) {
r.Handle("/api/v1/fleet/hosts/identifier/{identifier}", h.HostByIdentifier).Methods("GET").Name("host_by_identifier")
r.Handle("/api/v1/fleet/hosts/{id}", h.DeleteHost).Methods("DELETE").Name("delete_host")
r.Handle("/api/v1/fleet/spec/osquery_options", h.ApplyOsqueryOptionsSpec).Methods("POST").Name("apply_osquery_options_spec")
r.Handle("/api/v1/fleet/spec/osquery_options", h.GetOsqueryOptionsSpec).Methods("GET").Name("get_osquery_options_spec")
r.Handle("/api/v1/fleet/targets", h.SearchTargets).Methods("POST").Name("search_targets")
r.Handle("/api/v1/fleet/version", h.Version).Methods("GET").Name("version")

View File

@ -1,42 +0,0 @@
package service
import (
"context"
"time"
"github.com/fleetdm/fleet/server/contexts/viewer"
"github.com/fleetdm/fleet/server/kolide"
)
func (mw loggingMiddleware) GetOptionsSpec(ctx context.Context) (spec *kolide.OptionsSpec, err error) {
defer func(begin time.Time) {
_ = mw.loggerDebug(err).Log(
"method", "GetOptionsSpec",
"err", err,
"took", time.Since(begin),
)
}(time.Now())
spec, err = mw.Service.GetOptionsSpec(ctx)
return spec, err
}
func (mw loggingMiddleware) ApplyOptionsSpec(ctx context.Context, spec *kolide.OptionsSpec) (err error) {
var (
loggedInUser = "unauthenticated"
)
if vc, ok := viewer.FromContext(ctx); ok {
loggedInUser = vc.Username()
}
defer func(begin time.Time) {
_ = mw.loggerDebug(err).Log(
"method", "ApplyOptionsSpec",
"err", err,
"user", loggedInUser,
"took", time.Since(begin),
)
}(time.Now())
err = mw.Service.ApplyOptionsSpec(ctx, spec)
return err
}

View File

@ -0,0 +1,41 @@
package service
import (
"context"
"encoding/json"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
func (svc service) AgentOptionsForHost(ctx context.Context, host *kolide.Host) (json.RawMessage, error) {
// If host has a team and team has non-empty options, prioritize that.
if host.TeamID.Valid {
team, err := svc.ds.Team(uint(host.TeamID.Int64))
if err != nil {
return nil, errors.Wrap(err, "load team for host")
}
if team.AgentOptions != nil && len(*team.AgentOptions) > 0 {
var options kolide.AgentOptions
if err := json.Unmarshal(*team.AgentOptions, &options); err != nil {
return nil, errors.Wrap(err, "unmarshal team agent options")
}
return options.ForPlatform(host.Platform), nil
}
}
// Otherwise return the appropriate override for global options.
appConfig, err := svc.ds.AppConfig()
if err != nil {
return nil, errors.Wrap(err, "load global agent options")
}
var options kolide.AgentOptions
if err := json.Unmarshal(appConfig.AgentOptions, &options); err != nil {
return nil, errors.Wrap(err, "unmarshal global agent options")
}
return options.ForPlatform(host.Platform), nil
}

View File

@ -0,0 +1,54 @@
package service
import (
"context"
"encoding/json"
"testing"
"github.com/fleetdm/fleet/server/kolide"
"github.com/fleetdm/fleet/server/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
)
func TestAgentOptionsForHost(t *testing.T) {
ds := new(mock.Store)
svc, err := newTestService(ds, nil, nil)
require.NoError(t, err)
teamID := uint(1)
ds.TeamFunc = func(tid uint) (*kolide.Team, error) {
assert.Equal(t, teamID, tid)
opt := json.RawMessage(`{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}}`)
return &kolide.Team{AgentOptions: &opt}, nil
}
ds.AppConfigFunc = func() (*kolide.AppConfig, error) {
return &kolide.AppConfig{AgentOptions: json.RawMessage(`{"config":{"baz":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override2"}}}}`)}, nil
}
host := &kolide.Host{
TeamID: null.IntFrom(int64(teamID)),
Platform: "darwin",
}
opt, err := svc.AgentOptionsForHost(context.Background(), host)
require.NoError(t, err)
assert.JSONEq(t, `{"foo":"override"}`, string(opt))
host.Platform = "windows"
opt, err = svc.AgentOptionsForHost(context.Background(), host)
require.NoError(t, err)
assert.JSONEq(t, `{"foo":"bar"}`, string(opt))
// Should take gobal option with no team
host.TeamID.Valid = false
opt, err = svc.AgentOptionsForHost(context.Background(), host)
require.NoError(t, err)
assert.JSONEq(t, `{"baz":"bar"}`, string(opt))
host.Platform = "darwin"
opt, err = svc.AgentOptionsForHost(context.Background(), host)
require.NoError(t, err)
assert.JSONEq(t, `{"foo":"override2"}`, string(opt))
}

View File

@ -197,7 +197,7 @@ func (svc service) GetClientConfig(ctx context.Context) (map[string]interface{},
return nil, osqueryError{message: "internal error: missing host from request context"}
}
baseConfig, err := svc.ds.OptionsForPlatform(host.Platform)
baseConfig, err := svc.AgentOptionsForHost(ctx, &host)
if err != nil {
return nil, osqueryError{message: "internal error: fetching base config: " + err.Error()}
}

View File

@ -1,25 +0,0 @@
package service
import (
"context"
"github.com/fleetdm/fleet/server/kolide"
"github.com/pkg/errors"
)
func (svc service) ApplyOptionsSpec(ctx context.Context, spec *kolide.OptionsSpec) error {
err := svc.ds.ApplyOptions(spec)
if err != nil {
return errors.Wrap(err, "apply options")
}
return nil
}
func (svc service) GetOptionsSpec(ctx context.Context) (*kolide.OptionsSpec, error) {
spec, err := svc.ds.GetOptions()
if err != nil {
return nil, errors.Wrap(err, "get options from datastore")
}
return spec, nil
}

View File

@ -433,30 +433,8 @@ func TestGetClientConfig(t *testing.T) {
return []*kolide.ScheduledQuery{}, nil
}
}
ds.OptionsForPlatformFunc = func(platform string) (json.RawMessage, error) {
return json.RawMessage(`
{
"options":{
"distributed_interval":11,
"logger_tls_period":33
},
"decorators":{
"load":[
"SELECT version FROM osquery_info;",
"SELECT uuid AS host_uuid FROM system_info;"
],
"always":[
"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;"
],
"interval":{
"3600":[
"SELECT total_seconds AS uptime FROM uptime;"
]
}
},
"foo": "bar"
}
`), nil
ds.AppConfigFunc = func() (*kolide.AppConfig, error) {
return &kolide.AppConfig{AgentOptions: json.RawMessage(`{"config":{"options":{"baz":"bar"}}}`)}, nil
}
ds.SaveHostFunc = func(host *kolide.Host) error {
return nil
@ -469,27 +447,11 @@ func TestGetClientConfig(t *testing.T) {
ctx2 := hostctx.NewContext(context.Background(), kolide.Host{ID: 2})
expectedOptions := map[string]interface{}{
"distributed_interval": float64(11),
"logger_tls_period": float64(33),
}
expectedDecorators := map[string]interface{}{
"load": []interface{}{
"SELECT version FROM osquery_info;",
"SELECT uuid AS host_uuid FROM system_info;",
},
"always": []interface{}{
"SELECT user AS username FROM logged_in_users WHERE user <> '' ORDER BY time LIMIT 1;",
},
"interval": map[string]interface{}{
"3600": []interface{}{"SELECT total_seconds AS uptime FROM uptime;"},
},
"baz": "bar",
}
expectedConfig := map[string]interface{}{
"options": expectedOptions,
"decorators": expectedDecorators,
"foo": "bar",
"options": expectedOptions,
}
// No packs loaded yet
@ -1517,8 +1479,8 @@ func TestUpdateHostIntervals(t *testing.T) {
t.Run("", func(t *testing.T) {
ctx := hostctx.NewContext(context.Background(), tt.initHost)
ds.OptionsForPlatformFunc = func(platform string) (json.RawMessage, error) {
return tt.configOptions, nil
ds.AppConfigFunc = func() (*kolide.AppConfig, error) {
return &kolide.AppConfig{AgentOptions: json.RawMessage(`{"config":` + string(tt.configOptions) + `}`)}, nil
}
saveHostCalled := false

View File

@ -1,16 +0,0 @@
package service
import (
"context"
"encoding/json"
"net/http"
)
func decodeApplyOsqueryOptionsSpecRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req applyOsqueryOptionsSpecRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
return req, nil
}