mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Osquery options 365 (#657)
This commit is contained in:
parent
fc6a7a7921
commit
eec835a07a
22 changed files with 820 additions and 46 deletions
|
|
@ -328,12 +328,9 @@ func testSearchHosts(t *testing.T, ds kolide.Datastore) {
|
|||
hits, err = ds.SearchHosts("99.100.101")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 2, len(hits))
|
||||
|
||||
|
||||
hits, err = ds.SearchHosts("99.100.101", h3.ID)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, len(hits))
|
||||
|
||||
}
|
||||
|
||||
func testSearchHostsLimit(t *testing.T, ds kolide.Datastore) {
|
||||
|
|
|
|||
58
server/datastore/datastore_options_test.go
Normal file
58
server/datastore/datastore_options_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package datastore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/datastore/internal/appstate"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testOptions(t *testing.T, ds kolide.Datastore) {
|
||||
// were options pre-loaded?
|
||||
opts, err := ds.ListOptions()
|
||||
require.Nil(t, err)
|
||||
assert.Len(t, opts, len(appstate.Options))
|
||||
|
||||
opt, err := ds.OptionByName("aws_access_key_id")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, opt)
|
||||
opt2, err := ds.Option(opt.ID)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, opt2)
|
||||
assert.True(t, reflect.DeepEqual(opt, opt2))
|
||||
|
||||
opt.SetValue("somekey")
|
||||
err = ds.SaveOptions([]kolide.Option{*opt})
|
||||
require.Nil(t, err)
|
||||
opt, err = ds.Option(opt.ID)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "somekey", opt.GetValue())
|
||||
|
||||
// can't change a read only option
|
||||
opt, err = ds.OptionByName("disable_distributed")
|
||||
require.Nil(t, err)
|
||||
opt.SetValue(true)
|
||||
err = ds.SaveOptions([]kolide.Option{*opt})
|
||||
require.NotNil(t, err)
|
||||
|
||||
opt, _ = ds.OptionByName("aws_profile_name")
|
||||
assert.False(t, opt.OptionSet())
|
||||
opt.SetValue("zip")
|
||||
opt2, _ = ds.OptionByName("disable_distributed")
|
||||
assert.Equal(t, false, opt2.GetValue())
|
||||
opt2.SetValue(true)
|
||||
modList := []kolide.Option{*opt, *opt2}
|
||||
// The aws access key option can be saved but because the disable_events can't
|
||||
// be we want to verify that the whole transaction is rolled back
|
||||
err = ds.SaveOptions(modList)
|
||||
assert.NotNil(t, err)
|
||||
opt, _ = ds.OptionByName("aws_profile_name")
|
||||
assert.False(t, opt.OptionSet())
|
||||
opt2, err = ds.OptionByName("disable_distributed")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, false, opt2.GetValue())
|
||||
|
||||
}
|
||||
|
|
@ -55,5 +55,6 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
|
|||
testDeleteScheduledQuery,
|
||||
testListScheduledQueriesInPack,
|
||||
testSaveScheduledQuery,
|
||||
testOptions,
|
||||
testNewScheduledQuery,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ func (d *Datastore) NewAppConfig(info *kolide.AppConfig) (*kolide.AppConfig, err
|
|||
defer d.mtx.Unlock()
|
||||
|
||||
info.ID = 1
|
||||
d.orginfo = info
|
||||
d.appConfig = info
|
||||
return info, nil
|
||||
}
|
||||
|
||||
|
|
@ -15,8 +15,8 @@ func (d *Datastore) AppConfig() (*kolide.AppConfig, error) {
|
|||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
|
||||
if d.orginfo != nil {
|
||||
return d.orginfo, nil
|
||||
if d.appConfig != nil {
|
||||
return d.appConfig, nil
|
||||
}
|
||||
|
||||
return nil, notFound("AppConfig")
|
||||
|
|
@ -26,6 +26,6 @@ func (d *Datastore) SaveAppConfig(info *kolide.AppConfig) error {
|
|||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
|
||||
d.orginfo = info
|
||||
d.appConfig = info
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/datastore/internal/appstate"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/patrickmn/sortutil"
|
||||
)
|
||||
|
|
@ -31,9 +32,9 @@ type Datastore struct {
|
|||
distributedQueryExecutions map[uint]kolide.DistributedQueryExecution
|
||||
distributedQueryCampaigns map[uint]kolide.DistributedQueryCampaign
|
||||
distributedQueryCampaignTargets map[uint]kolide.DistributedQueryCampaignTarget
|
||||
|
||||
orginfo *kolide.AppConfig
|
||||
config *config.KolideConfig
|
||||
options map[uint]*kolide.Option
|
||||
appConfig *kolide.AppConfig
|
||||
config *config.KolideConfig
|
||||
}
|
||||
|
||||
func New(config config.KolideConfig) (*Datastore, error) {
|
||||
|
|
@ -68,32 +69,44 @@ func sortResults(slice interface{}, opt kolide.ListOptions, fields map[string]st
|
|||
return nil
|
||||
}
|
||||
|
||||
func (orm *Datastore) Migrate() error {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
orm.nextIDs = make(map[interface{}]uint)
|
||||
orm.users = make(map[uint]*kolide.User)
|
||||
orm.sessions = make(map[uint]*kolide.Session)
|
||||
orm.passwordResets = make(map[uint]*kolide.PasswordResetRequest)
|
||||
orm.invites = make(map[uint]*kolide.Invite)
|
||||
orm.labels = make(map[uint]*kolide.Label)
|
||||
orm.labelQueryExecutions = make(map[uint]*kolide.LabelQueryExecution)
|
||||
orm.queries = make(map[uint]*kolide.Query)
|
||||
orm.packs = make(map[uint]*kolide.Pack)
|
||||
orm.hosts = make(map[uint]*kolide.Host)
|
||||
orm.scheduledQueries = make(map[uint]*kolide.ScheduledQuery)
|
||||
orm.packTargets = make(map[uint]*kolide.PackTarget)
|
||||
orm.distributedQueryExecutions = make(map[uint]kolide.DistributedQueryExecution)
|
||||
orm.distributedQueryCampaigns = make(map[uint]kolide.DistributedQueryCampaign)
|
||||
orm.distributedQueryCampaignTargets = make(map[uint]kolide.DistributedQueryCampaignTarget)
|
||||
orm.orginfo = &kolide.AppConfig{
|
||||
func (d *Datastore) Migrate() error {
|
||||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
d.nextIDs = make(map[interface{}]uint)
|
||||
d.users = make(map[uint]*kolide.User)
|
||||
d.sessions = make(map[uint]*kolide.Session)
|
||||
d.passwordResets = make(map[uint]*kolide.PasswordResetRequest)
|
||||
d.invites = make(map[uint]*kolide.Invite)
|
||||
d.labels = make(map[uint]*kolide.Label)
|
||||
d.labelQueryExecutions = make(map[uint]*kolide.LabelQueryExecution)
|
||||
d.queries = make(map[uint]*kolide.Query)
|
||||
d.packs = make(map[uint]*kolide.Pack)
|
||||
d.hosts = make(map[uint]*kolide.Host)
|
||||
d.scheduledQueries = make(map[uint]*kolide.ScheduledQuery)
|
||||
d.packTargets = make(map[uint]*kolide.PackTarget)
|
||||
d.distributedQueryExecutions = make(map[uint]kolide.DistributedQueryExecution)
|
||||
d.distributedQueryCampaigns = make(map[uint]kolide.DistributedQueryCampaign)
|
||||
d.distributedQueryCampaignTargets = make(map[uint]kolide.DistributedQueryCampaignTarget)
|
||||
d.options = make(map[uint]*kolide.Option)
|
||||
|
||||
for _, initData := range appstate.Options {
|
||||
opt := kolide.Option{
|
||||
Name: initData.Name,
|
||||
Value: kolide.OptionValue{Val: initData.Value},
|
||||
Type: initData.Type,
|
||||
ReadOnly: initData.ReadOnly,
|
||||
}
|
||||
opt.ID = d.nextID(opt)
|
||||
d.options[opt.ID] = &opt
|
||||
}
|
||||
|
||||
d.appConfig = &kolide.AppConfig{
|
||||
ID: 1,
|
||||
SMTPEnableTLS: true,
|
||||
SMTPPort: 587,
|
||||
SMTPEnableStartTLS: true,
|
||||
SMTPVerifySSLCerts: true,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
74
server/datastore/inmem/options.go
Normal file
74
server/datastore/inmem/options.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package inmem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/patrickmn/sortutil"
|
||||
)
|
||||
|
||||
func (d *Datastore) OptionByName(name string) (*kolide.Option, error) {
|
||||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
for _, opt := range d.options {
|
||||
if opt.Name == name {
|
||||
result := *opt
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
return nil, notFound("options")
|
||||
}
|
||||
|
||||
type optPair struct {
|
||||
newOpt kolide.Option
|
||||
existingOpt *kolide.Option
|
||||
}
|
||||
|
||||
func (d *Datastore) SaveOptions(opts []kolide.Option) error {
|
||||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
var validPairs []optPair
|
||||
for _, opt := range opts {
|
||||
if opt.ReadOnly {
|
||||
return fmt.Errorf("readonly option can't be changed")
|
||||
}
|
||||
existing, ok := d.options[opt.ID]
|
||||
if !ok {
|
||||
return notFound("option")
|
||||
}
|
||||
if existing.Type != opt.Type {
|
||||
return fmt.Errorf("type mismatch for option")
|
||||
}
|
||||
validPairs = append(validPairs, optPair{opt, existing})
|
||||
}
|
||||
// if all the options to be modified pass validation copy values over to
|
||||
// existing options
|
||||
if len(validPairs) == len(opts) {
|
||||
for _, pair := range validPairs {
|
||||
pair.existingOpt.Value.Val = pair.newOpt.Value.Val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Datastore) Option(id uint) (*kolide.Option, error) {
|
||||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
saved, ok := d.options[id]
|
||||
if !ok {
|
||||
return nil, notFound("Option").WithID(id)
|
||||
}
|
||||
result := *saved
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) ListOptions() ([]kolide.Option, error) {
|
||||
d.mtx.Lock()
|
||||
defer d.mtx.Unlock()
|
||||
result := []kolide.Option{}
|
||||
for _, opt := range d.options {
|
||||
result = append(result, *opt)
|
||||
}
|
||||
sortutil.AscByField(result, "Name")
|
||||
return result, nil
|
||||
}
|
||||
70
server/datastore/internal/appstate/options.go
Normal file
70
server/datastore/internal/appstate/options.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package appstate
|
||||
|
||||
import "github.com/kolide/kolide-ose/server/kolide"
|
||||
|
||||
var Options = []struct {
|
||||
Name string
|
||||
Value interface{}
|
||||
Type kolide.OptionType
|
||||
ReadOnly bool
|
||||
}{
|
||||
// These options are read only, attempting to modify one of these will
|
||||
// raise an error
|
||||
{"disable_distributed", false, kolide.OptionTypeBool, kolide.ReadOnly},
|
||||
{"distributed_plugin", "tls", kolide.OptionTypeString, kolide.ReadOnly},
|
||||
{"distributed_tls_read_endpoint", "/api/v1/osquery/distributed/read", kolide.OptionTypeString, kolide.ReadOnly},
|
||||
{"distributed_tls_write_endpoint", "/api/v1/osquery/distributed/write", kolide.OptionTypeString, kolide.ReadOnly},
|
||||
{"pack_delimiter", "/", kolide.OptionTypeString, kolide.ReadOnly},
|
||||
// These options may be modified by an admin user
|
||||
{"aws_access_key_id", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_firehose_period", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"aws_firehose_stream", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_kinesis_period", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"aws_kinesis_random_partition_key", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"aws_kinesis_stream", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_profile_name", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_region", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_secret_access_key", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_sts_arn_role", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_sts_region", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_sts_session_name", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"aws_sts_timeout", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"buffered_log_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"decorations_top_level", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_caching", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_database", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_decorators", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_events", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_kernel", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_logging", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"disable_tables", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"distributed_interval", 10, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"distributed_tls_max_attempts", 3, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"enable_foreign", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"enable_monitor", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"ephemeral", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"events_expiry", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"events_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"events_optimize", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"host_identifier", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"logger_event_type", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"logger_mode", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"logger_path", nil, kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"logger_plugin", "tls", kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"logger_secondary_status_only", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"logger_syslog_facility", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"logger_tls_compress", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"logger_tls_endpoint", "/api/v1/osquery/log", kolide.OptionTypeString, kolide.NotReadOnly},
|
||||
{"logger_tls_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"logger_tls_period", 10, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"pack_refresh_interval", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"read_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"read_user_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"schedule_default_interval", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"schedule_splay_percent", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"schedule_timeout", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"utc", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"value_max", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
{"verbose", nil, kolide.OptionTypeBool, kolide.NotReadOnly},
|
||||
{"worker_threads", nil, kolide.OptionTypeInt, kolide.NotReadOnly},
|
||||
}
|
||||
|
|
@ -11,22 +11,25 @@ func init() {
|
|||
}
|
||||
|
||||
func Up_20161118212604(tx *sql.Tx) error {
|
||||
|
||||
_, err := tx.Exec(
|
||||
"CREATE TABLE `options` (" +
|
||||
"`id` int(10) unsigned NOT NULL AUTO_INCREMENT," +
|
||||
"`created_at` timestamp DEFAULT CURRENT_TIMESTAMP," +
|
||||
"`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
|
||||
"`key` varchar(255) NOT NULL," +
|
||||
"`value` varchar(255) NOT NULL," +
|
||||
"`platform` varchar(255) DEFAULT NULL," +
|
||||
"`id` INT UNSIGNED NOT NULL AUTO_INCREMENT," +
|
||||
"`name` VARCHAR(255) NOT NULL," +
|
||||
"`type` INT UNSIGNED NOT NULL," +
|
||||
"`value` VARCHAR(255) NOT NULL," +
|
||||
"`read_only` TINYINT(1) NOT NULL DEFAULT FALSE," +
|
||||
"PRIMARY KEY (`id`)," +
|
||||
"UNIQUE KEY `idx_option_unique_key` (`key`)" +
|
||||
"UNIQUE KEY `idx_option_unique_name` (`name`)" +
|
||||
") ENGINE=InnoDB DEFAULT CHARSET=utf8;",
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func Down_20161118212604(tx *sql.Tx) error {
|
||||
|
||||
_, err := tx.Exec("DROP TABLE IF EXISTS `options`;")
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/datastore/internal/appstate"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up_20161223115449, Down_20161223115449)
|
||||
}
|
||||
|
||||
func Up_20161223115449(tx *sql.Tx) error {
|
||||
sqlStatement := `
|
||||
INSERT INTO options (
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
read_only
|
||||
) VALUES( ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
for _, opt := range appstate.Options {
|
||||
ov := kolide.Option{
|
||||
Name: opt.Name,
|
||||
ReadOnly: opt.ReadOnly,
|
||||
Type: opt.Type,
|
||||
Value: kolide.OptionValue{
|
||||
Val: opt.Value,
|
||||
},
|
||||
}
|
||||
_, err := tx.Exec(sqlStatement, ov.Name, ov.Type, ov.Value, ov.ReadOnly)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20161223115449(tx *sql.Tx) error {
|
||||
sqlStatement := `
|
||||
DELETE FROM options
|
||||
WHERE name = ?
|
||||
`
|
||||
for _, opt := range appstate.Options {
|
||||
_, err := tx.Exec(sqlStatement, opt.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
96
server/datastore/mysql/options.go
Normal file
96
server/datastore/mysql/options.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (d *Datastore) OptionByName(name string) (*kolide.Option, error) {
|
||||
sqlStatement := `
|
||||
SELECT *
|
||||
FROM options
|
||||
WHERE name = ?
|
||||
`
|
||||
var option kolide.Option
|
||||
if err := d.db.Get(&option, sqlStatement, name); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, notFound("Option")
|
||||
}
|
||||
return nil, errors.Wrap(err, sqlStatement)
|
||||
}
|
||||
return &option, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) SaveOptions(opts []kolide.Option) (err error) {
|
||||
sqlStatement := `
|
||||
UPDATE options
|
||||
SET value = ?
|
||||
WHERE id = ? AND type = ? AND NOT read_only
|
||||
`
|
||||
txn, err := d.db.Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "update options begin transaction")
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if success {
|
||||
if err = txn.Commit(); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
txn.Rollback()
|
||||
}()
|
||||
|
||||
for _, opt := range opts {
|
||||
result, err := txn.Exec(sqlStatement, opt.Value, opt.ID, opt.Type)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "update options")
|
||||
}
|
||||
rowsChanged, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "option rows affected")
|
||||
}
|
||||
if rowsChanged != 1 {
|
||||
return notFound("Option").WithID(opt.ID)
|
||||
}
|
||||
}
|
||||
// If all the updates succeed, set the success flag, this will cause the
|
||||
// function we defined in defer to commit the transaction. Otherwise, all
|
||||
// changes will be rolled back
|
||||
success = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Datastore) Option(id uint) (*kolide.Option, error) {
|
||||
sqlStatement := `
|
||||
SELECT *
|
||||
FROM options
|
||||
WHERE id = ?
|
||||
`
|
||||
var opt kolide.Option
|
||||
if err := d.db.Get(&opt, sqlStatement, id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, notFound("Option").WithID(id)
|
||||
}
|
||||
return nil, errors.Wrap(err, "select option by ID")
|
||||
}
|
||||
return &opt, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) ListOptions() ([]kolide.Option, error) {
|
||||
sqlStatement := `
|
||||
SELECT *
|
||||
FROM options
|
||||
ORDER BY name ASC
|
||||
`
|
||||
var opts []kolide.Option
|
||||
if err := d.db.Select(&opts, sqlStatement); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, notFound("Option")
|
||||
}
|
||||
return nil, errors.Wrap(err, "select from options")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ type Datastore interface {
|
|||
AppConfigStore
|
||||
InviteStore
|
||||
ScheduledQueryStore
|
||||
OptionStore
|
||||
Name() string
|
||||
Drop() error
|
||||
Migrate() error
|
||||
|
|
|
|||
153
server/kolide/options.go
Normal file
153
server/kolide/options.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package kolide
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// OptionStore interface describes methods to access datastore
|
||||
type OptionStore interface {
|
||||
// SaveOptions transactional write of options to storage. If one or more
|
||||
// values fails validation none of the writes will succeed. Note only option
|
||||
// values are written. Other option fields are created in migration and do
|
||||
// not change. Attempting to write ReadOnly options will cause an error.
|
||||
SaveOptions(opts []Option) error
|
||||
// Options returns all options
|
||||
ListOptions() ([]Option, error)
|
||||
// Option return an option by ID
|
||||
Option(id uint) (*Option, error)
|
||||
// OptionByName returns an option uniquely identified by name
|
||||
OptionByName(name string) (*Option, error)
|
||||
}
|
||||
|
||||
// OptionService interface describes methods that operate on osquery options
|
||||
type OptionService interface {
|
||||
// GetOptions retrieves all options
|
||||
GetOptions(ctx context.Context) ([]Option, error)
|
||||
// ModifyOptions will change values of the options in OptionRequest. Note
|
||||
// passing ReadOnly options will cause an error.
|
||||
ModifyOptions(ctx context.Context, req OptionRequest) ([]Option, error)
|
||||
}
|
||||
|
||||
const (
|
||||
ReadOnly = true
|
||||
NotReadOnly = !ReadOnly
|
||||
)
|
||||
|
||||
// OptionType defines the type of the value assigned to an option
|
||||
type OptionType int
|
||||
|
||||
const (
|
||||
OptionTypeString OptionType = iota
|
||||
OptionTypeInt
|
||||
OptionTypeBool
|
||||
)
|
||||
|
||||
// String values that map from JSON to OptionType
|
||||
const (
|
||||
optionTypeString = "string"
|
||||
optionTypeInt = "int"
|
||||
optionTypeBool = "bool"
|
||||
)
|
||||
|
||||
// MarshalJSON marshals option type to strings
|
||||
func (ot OptionType) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(`"%s"`, ot)), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts json to OptionType
|
||||
func (ot *OptionType) UnmarshalJSON(b []byte) error {
|
||||
switch typ := string(b); strings.Trim(typ, `"`) {
|
||||
case optionTypeString:
|
||||
*ot = OptionTypeString
|
||||
case optionTypeBool:
|
||||
*ot = OptionTypeBool
|
||||
case optionTypeInt:
|
||||
*ot = OptionTypeInt
|
||||
default:
|
||||
return fmt.Errorf("unsupported option type '%s'", typ)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String is used to marshal OptionType to human readable strings used in JSON payloads
|
||||
func (ot OptionType) String() string {
|
||||
switch ot {
|
||||
case OptionTypeString:
|
||||
return optionTypeString
|
||||
case OptionTypeInt:
|
||||
return optionTypeInt
|
||||
case OptionTypeBool:
|
||||
return optionTypeBool
|
||||
default:
|
||||
panic("stringer not implemented for OptionType")
|
||||
}
|
||||
}
|
||||
|
||||
// OptionValue supports Valuer and Scan interfaces for reading and writing
|
||||
// to the database, and also JSON marshaling
|
||||
type OptionValue struct {
|
||||
Val interface{}
|
||||
}
|
||||
|
||||
// Value is called by the DB driver. Note that we store data as JSON
|
||||
// types, so we can use the JSON marshaller to assign the appropriate
|
||||
// type when we fetch it from the db
|
||||
func (ov OptionValue) Value() (dv driver.Value, err error) {
|
||||
return json.Marshal(ov.Val)
|
||||
}
|
||||
|
||||
// Scan takes db string and turns it into an option type
|
||||
func (ov *OptionValue) Scan(src interface{}) error {
|
||||
return json.Unmarshal(src.([]byte), &ov.Val)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface
|
||||
func (ov OptionValue) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(ov.Val)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface
|
||||
func (ov *OptionValue) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, &ov.Val)
|
||||
}
|
||||
|
||||
// Option represents a possible osquery confguration option
|
||||
// See https://osquery.readthedocs.io/en/stable/deployment/configuration/
|
||||
type Option struct {
|
||||
// ID unique identifier for option assigned by the dbms
|
||||
ID uint `json:"id"`
|
||||
// Name of the option which must be unique
|
||||
Name string `json:"name"`
|
||||
// Type of value for the option
|
||||
Type OptionType `json:"type"`
|
||||
// Value of the option which may be nil, bool, int, or string.
|
||||
Value OptionValue `json:"value"`
|
||||
// ReadOnly if true, this option is required for Kolide to function
|
||||
// properly and cannot be modified by the end user
|
||||
ReadOnly bool `json:"read_only" db:"read_only"`
|
||||
}
|
||||
|
||||
// SetValue sets the value associated with the option
|
||||
func (opt *Option) SetValue(v interface{}) {
|
||||
opt.Value.Val = v
|
||||
}
|
||||
|
||||
// OptionSet returns true if the option has a value assigned to it
|
||||
func (opt *Option) OptionSet() bool {
|
||||
return opt.Value.Val != nil
|
||||
}
|
||||
|
||||
// GetValue returns the value associated with the option
|
||||
func (opt Option) GetValue() interface{} {
|
||||
return opt.Value.Val
|
||||
}
|
||||
|
||||
// OptionRequest contains options that are passed to modify options requests.
|
||||
type OptionRequest struct {
|
||||
Options []Option `json:"options"`
|
||||
}
|
||||
47
server/kolide/options_test.go
Normal file
47
server/kolide/options_test.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package kolide
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOptionMarshaller(t *testing.T) {
|
||||
tests := []struct {
|
||||
value interface{}
|
||||
typ OptionType
|
||||
expected interface{}
|
||||
}{
|
||||
{23, OptionTypeInt, float64(23)},
|
||||
{true, OptionTypeBool, true},
|
||||
{"foobar", OptionTypeString, "foobar"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
optIn := &Option{1, "foo", test.typ, OptionValue{test.value}, true}
|
||||
buff, err := json.Marshal(optIn)
|
||||
require.Nil(t, err)
|
||||
optOut := &Option{}
|
||||
err = json.Unmarshal(buff, optOut)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, optIn.ID, optOut.ID)
|
||||
assert.Equal(t, optIn.Name, optOut.Name)
|
||||
assert.Equal(t, optIn.ReadOnly, optOut.ReadOnly)
|
||||
assert.Equal(t, optIn.Type, optOut.Type)
|
||||
assert.Equal(t, test.expected, optOut.Value.Val)
|
||||
|
||||
}
|
||||
|
||||
// test nil
|
||||
optIn := &Option{1, "bar", OptionTypeString, OptionValue{nil}, true}
|
||||
buff, err := json.Marshal(optIn)
|
||||
require.Nil(t, err)
|
||||
optOut := &Option{}
|
||||
err = json.Unmarshal(buff, optOut)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, reflect.DeepEqual(optIn, optOut))
|
||||
|
||||
}
|
||||
|
|
@ -64,15 +64,6 @@ type Query struct {
|
|||
Packs []Pack `json:"packs" db:"-"`
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Key string
|
||||
Value string
|
||||
Platform string
|
||||
}
|
||||
|
||||
type DecoratorType int
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ type Service interface {
|
|||
InviteService
|
||||
TargetService
|
||||
ScheduledQueryService
|
||||
OptionService
|
||||
}
|
||||
|
|
|
|||
35
server/service/endpoint_options.go
Normal file
35
server/service/endpoint_options.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type optionsResponse struct {
|
||||
Options []kolide.Option `json:"options,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (or optionsResponse) error() error { return or.Err }
|
||||
|
||||
func makeGetOptionsEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
options, err := svc.GetOptions(ctx)
|
||||
if err != nil {
|
||||
return optionsResponse{Err: err}, nil
|
||||
}
|
||||
return optionsResponse{Options: options}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func makeModifyOptionsEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
payload := request.(kolide.OptionRequest)
|
||||
opts, err := svc.ModifyOptions(ctx, payload)
|
||||
if err != nil {
|
||||
return optionsResponse{Err: err}, nil
|
||||
}
|
||||
return optionsResponse{Options: opts}, nil
|
||||
}
|
||||
}
|
||||
69
server/service/endpoint_options_test.go
Normal file
69
server/service/endpoint_options_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testGetOptions(t *testing.T, r *testResource) {
|
||||
|
||||
req, err := http.NewRequest("GET", r.server.URL+"/api/v1/kolide/options", nil)
|
||||
require.Nil(t, err)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken))
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
var optsResp optionsResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&optsResp)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, optsResp.Options)
|
||||
assert.Equal(t, "aws_access_key_id", optsResp.Options[0].Name)
|
||||
}
|
||||
|
||||
func testModifyOptions(t *testing.T, r *testResource) {
|
||||
inJson := `{"options":[
|
||||
{"id":6,"name":"aws_access_key_id","type":"string","value":"foo","read_only":false},
|
||||
{"id":7,"name":"aws_firehose_period","type":"int","value":23,"read_only":false}]}`
|
||||
buff := bytes.NewBufferString(inJson)
|
||||
req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/options", buff)
|
||||
require.Nil(t, err)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken))
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.Nil(t, err)
|
||||
|
||||
var optsResp optionsResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&optsResp)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, optsResp.Options)
|
||||
require.Len(t, optsResp.Options, 2)
|
||||
assert.Equal(t, "foo", optsResp.Options[0].GetValue())
|
||||
assert.Equal(t, float64(23), optsResp.Options[1].GetValue())
|
||||
}
|
||||
|
||||
func testModifyOptionsValidationFail(t *testing.T, r *testResource) {
|
||||
inJson := `{"options":[
|
||||
{"id":6,"name":"aws_access_key_id","type":"string","value":"foo","read_only":false},
|
||||
{"id":7,"name":"aws_firehose_period","type":"int","value":"xxs","read_only":false}]}`
|
||||
buff := bytes.NewBufferString(inJson)
|
||||
req, err := http.NewRequest("PATCH", r.server.URL+"/api/v1/kolide/options", buff)
|
||||
require.Nil(t, err)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.userToken))
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
|
||||
var errStruct mockValidationError
|
||||
err = json.NewDecoder(resp.Body).Decode(&errStruct)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, errStruct.Errors, 1)
|
||||
assert.Equal(t, "aws_firehose_period", errStruct.Errors[0].Name)
|
||||
assert.Equal(t, "type mismatch", errStruct.Errors[0].Reason)
|
||||
}
|
||||
|
|
@ -89,6 +89,9 @@ var testFunctions = [...]func(*testing.T, *testResource){
|
|||
testGetAppConfig,
|
||||
testModifyAppConfig,
|
||||
testModifyAppConfigWithValidationFail,
|
||||
testGetOptions,
|
||||
testModifyOptions,
|
||||
testModifyOptionsValidationFail,
|
||||
}
|
||||
|
||||
func TestEndpoints(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ type KolideEndpoints struct {
|
|||
DeleteHost endpoint.Endpoint
|
||||
ListHosts endpoint.Endpoint
|
||||
SearchTargets endpoint.Endpoint
|
||||
GetOptions endpoint.Endpoint
|
||||
ModifyOptions endpoint.Endpoint
|
||||
}
|
||||
|
||||
// MakeKolideServerEndpoints creates the Kolide API endpoints.
|
||||
|
|
@ -127,6 +129,8 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint
|
|||
GetLabelsForPack: authenticatedUser(jwtKey, svc, makeGetLabelsForPackEndpoint(svc)),
|
||||
DeleteLabelFromPack: authenticatedUser(jwtKey, svc, makeDeleteLabelFromPackEndpoint(svc)),
|
||||
SearchTargets: authenticatedUser(jwtKey, svc, makeSearchTargetsEndpoint(svc)),
|
||||
GetOptions: authenticatedUser(jwtKey, svc, mustBeAdmin(makeGetOptionsEndpoint(svc))),
|
||||
ModifyOptions: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyOptionsEndpoint(svc))),
|
||||
|
||||
// Osquery endpoints
|
||||
EnrollAgent: makeEnrollAgentEndpoint(svc),
|
||||
|
|
@ -190,6 +194,8 @@ type kolideHandlers struct {
|
|||
DeleteHost http.Handler
|
||||
ListHosts http.Handler
|
||||
SearchTargets http.Handler
|
||||
GetOptions http.Handler
|
||||
ModifyOptions http.Handler
|
||||
}
|
||||
|
||||
func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithttp.ServerOption) *kolideHandlers {
|
||||
|
|
@ -249,6 +255,8 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
|
|||
DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest),
|
||||
ListHosts: newServer(e.ListHosts, decodeListHostsRequest),
|
||||
SearchTargets: newServer(e.SearchTargets, decodeSearchTargetsRequest),
|
||||
GetOptions: newServer(e.GetOptions, decodeNoParamsRequest),
|
||||
ModifyOptions: newServer(e.ModifyOptions, decodeModifyOptionsRequest),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,6 +351,9 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) {
|
|||
r.Handle("/api/v1/kolide/hosts/{id}", h.GetHost).Methods("GET").Name("get_host")
|
||||
r.Handle("/api/v1/kolide/hosts/{id}", h.DeleteHost).Methods("DELETE").Name("delete_host")
|
||||
|
||||
r.Handle("/api/v1/kolide/options", h.GetOptions).Methods("GET").Name("get_options")
|
||||
r.Handle("/api/v1/kolide/options", h.ModifyOptions).Methods("PATCH").Name("modify_options")
|
||||
|
||||
r.Handle("/api/v1/kolide/targets", h.SearchTargets).Methods("POST").Name("search_targets")
|
||||
|
||||
r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST").Name("enroll_agent")
|
||||
|
|
|
|||
22
server/service/service_options.go
Normal file
22
server/service/service_options.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (svc service) GetOptions(ctx context.Context) ([]kolide.Option, error) {
|
||||
opts, err := svc.ds.ListOptions()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "options service")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (svc service) ModifyOptions(ctx context.Context, req kolide.OptionRequest) ([]kolide.Option, error) {
|
||||
if err := svc.ds.SaveOptions(req.Options); err != nil {
|
||||
return nil, errors.Wrap(err, "modify options service")
|
||||
}
|
||||
return req.Options, nil
|
||||
}
|
||||
17
server/service/transport_options.go
Normal file
17
server/service/transport_options.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func decodeModifyOptionsRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req kolide.OptionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
57
server/service/validation_options.go
Normal file
57
server/service/validation_options.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (mw validationMiddleware) ModifyOptions(ctx context.Context, req kolide.OptionRequest) ([]kolide.Option, error) {
|
||||
invalid := &invalidArgumentError{}
|
||||
for _, opt := range req.Options {
|
||||
if opt.ReadOnly {
|
||||
invalid.Append(opt.Name, "readonly option")
|
||||
continue
|
||||
}
|
||||
if err := validateValueMapsToOptionType(opt); err != nil {
|
||||
invalid.Append(opt.Name, err.Error())
|
||||
}
|
||||
}
|
||||
if invalid.HasErrors() {
|
||||
return nil, invalid
|
||||
}
|
||||
return mw.Service.ModifyOptions(ctx, req)
|
||||
}
|
||||
|
||||
var (
|
||||
errTypeMismatch = fmt.Errorf("type mismatch")
|
||||
errInvalidType = fmt.Errorf("invalid option type")
|
||||
)
|
||||
|
||||
func validateValueMapsToOptionType(opt kolide.Option) error {
|
||||
if !opt.OptionSet() {
|
||||
return nil
|
||||
}
|
||||
val := opt.GetValue()
|
||||
switch opt.Type {
|
||||
case kolide.OptionTypeBool:
|
||||
_, ok := val.(bool)
|
||||
if !ok {
|
||||
return errTypeMismatch
|
||||
}
|
||||
case kolide.OptionTypeString:
|
||||
_, ok := val.(string)
|
||||
if !ok {
|
||||
return errTypeMismatch
|
||||
}
|
||||
case kolide.OptionTypeInt:
|
||||
_, ok := val.(float64) // JSON unmarshaler represents all numbers in float64
|
||||
if !ok {
|
||||
return errTypeMismatch
|
||||
}
|
||||
default:
|
||||
return errInvalidType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in a new issue