Osquery options 365 (#657)

This commit is contained in:
John Murphy 2016-12-29 12:32:28 -06:00 committed by GitHub
parent fc6a7a7921
commit eec835a07a
22 changed files with 820 additions and 46 deletions

View file

@ -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) {

View 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())
}

View file

@ -55,5 +55,6 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testDeleteScheduledQuery,
testListScheduledQueriesInPack,
testSaveScheduledQuery,
testOptions,
testNewScheduledQuery,
}

View file

@ -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
}

View file

@ -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
}

View 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
}

View 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},
}

View file

@ -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
}

View file

@ -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
}

View 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
}

View file

@ -13,6 +13,7 @@ type Datastore interface {
AppConfigStore
InviteStore
ScheduledQueryStore
OptionStore
Name() string
Drop() error
Migrate() error

153
server/kolide/options.go Normal file
View 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"`
}

View 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))
}

View file

@ -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 (

View file

@ -14,4 +14,5 @@ type Service interface {
InviteService
TargetService
ScheduledQueryService
OptionService
}

View 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
}
}

View 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)
}

View file

@ -89,6 +89,9 @@ var testFunctions = [...]func(*testing.T, *testResource){
testGetAppConfig,
testModifyAppConfig,
testModifyAppConfigWithValidationFail,
testGetOptions,
testModifyOptions,
testModifyOptionsValidationFail,
}
func TestEndpoints(t *testing.T) {

View file

@ -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")

View 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
}

View 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
}

View 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
}