From a03347489c9afa0f3897396528984dca3eeb3bb0 Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Sun, 2 Oct 2016 20:14:35 -0700 Subject: [PATCH] Osquery Configuration Control (#244) Label management APIs and an osquery config endpoint based on active pack and label state. --- server/datastore/datastore_test.go | 65 +++++++- server/datastore/gorm.go | 187 ++++++++++++++++++++---- server/datastore/gorm_test.go | 5 + server/datastore/inmem_osquery.go | 4 +- server/kolide/datastore.go | 2 +- server/kolide/labels.go | 60 ++++++++ server/kolide/osquery.go | 31 +--- server/kolide/packs.go | 26 +++- server/kolide/queries.go | 47 +----- server/kolide/service.go | 1 + server/service/endpoint_labels.go | 158 ++++++++++++++++++++ server/service/endpoint_packs.go | 87 +++++++++++ server/service/handler.go | 40 +++++ server/service/handler_test.go | 32 ++++ server/service/service_labels.go | 59 ++++++++ server/service/service_labels_test.go | 133 +++++++++++++++++ server/service/service_osquery.go | 49 ++++++- server/service/service_osquery_test.go | 84 +++++++++++ server/service/service_packs.go | 65 ++++---- server/service/service_packs_test.go | 4 +- server/service/transport_labels.go | 49 +++++++ server/service/transport_labels_test.go | 90 ++++++++++++ server/service/transport_packs.go | 40 +++++ 23 files changed, 1176 insertions(+), 142 deletions(-) create mode 100644 server/service/endpoint_labels.go create mode 100644 server/service/service_labels.go create mode 100644 server/service/service_labels_test.go create mode 100644 server/service/transport_labels.go create mode 100644 server/service/transport_labels_test.go diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index 49095283c0..e517f48c6f 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -298,7 +298,7 @@ func testLabels(t *testing.T, db kolide.Datastore) { assert.Empty(t, queries) // No labels should match - labels, err := db.LabelsForHost(host) + labels, err := db.LabelsForHost(host.ID) assert.Nil(t, err) assert.Empty(t, labels) @@ -384,7 +384,7 @@ func testLabels(t *testing.T, db kolide.Datastore) { assert.Equal(t, expectQueries, queries) // No labels should match with no results yet - labels, err = db.LabelsForHost(host) + labels, err = db.LabelsForHost(host.ID) assert.Nil(t, err) assert.Empty(t, labels) @@ -417,7 +417,7 @@ func testLabels(t *testing.T, db kolide.Datastore) { assert.Equal(t, expectQueries, queries) // Now the two matching labels should be returned - labels, err = db.LabelsForHost(host) + labels, err = db.LabelsForHost(host.ID) assert.Nil(t, err) if assert.Len(t, labels, 2) { labelNames := []string{labels[0].Name, labels[1].Name} @@ -435,7 +435,7 @@ func testLabels(t *testing.T, db kolide.Datastore) { // There should still be no labels returned for a host that never // executed any label queries - labels, err = db.LabelsForHost(&hosts[0]) + labels, err = db.LabelsForHost(hosts[0].ID) assert.Nil(t, err) assert.Empty(t, labels) } @@ -529,7 +529,7 @@ func testDeletePack(t *testing.T, ds kolide.Datastore) { pack, err = ds.Pack(pack.ID) assert.Nil(t, err) - err = ds.DeletePack(pack) + err = ds.DeletePack(pack.ID) assert.Nil(t, err) assert.NotEqual(t, pack.ID, 0) @@ -550,7 +550,7 @@ func testAddAndRemoveQueryFromPack(t *testing.T, ds kolide.Datastore) { } _, err = ds.NewQuery(q1) assert.Nil(t, err) - err = ds.AddQueryToPack(q1, pack) + err = ds.AddQueryToPack(q1.ID, pack.ID) assert.Nil(t, err) q2 := &kolide.Query{ @@ -559,7 +559,7 @@ func testAddAndRemoveQueryFromPack(t *testing.T, ds kolide.Datastore) { } _, err = ds.NewQuery(q2) assert.Nil(t, err) - err = ds.AddQueryToPack(q2, pack) + err = ds.AddQueryToPack(q2.ID, pack.ID) assert.Nil(t, err) queries, err := ds.GetQueriesInPack(pack) @@ -573,3 +573,54 @@ func testAddAndRemoveQueryFromPack(t *testing.T, ds kolide.Datastore) { assert.Nil(t, err) assert.Len(t, queries, 1) } + +func testManagingLabelsOnPacks(t *testing.T, ds kolide.Datastore) { + mysqlQuery := &kolide.Query{ + Name: "MySQL", + Query: "select pid from processes where name = 'mysqld';", + } + mysqlQuery, err := ds.NewQuery(mysqlQuery) + assert.Nil(t, err) + + osqueryRunningQuery := &kolide.Query{ + Name: "Is osquery currently running?", + Query: "select pid from processes where name = 'osqueryd';", + } + osqueryRunningQuery, err = ds.NewQuery(osqueryRunningQuery) + assert.Nil(t, err) + + monitoringPack := &kolide.Pack{ + Name: "monitoring", + } + err = ds.NewPack(monitoringPack) + assert.Nil(t, err) + + mysqlLabel := &kolide.Label{ + Name: "MySQL Monitoring", + QueryID: mysqlQuery.ID, + } + mysqlLabel, err = ds.NewLabel(mysqlLabel) + assert.Nil(t, err) + + err = ds.AddLabelToPack(mysqlLabel.ID, monitoringPack.ID) + assert.Nil(t, err) + + labels, err := ds.GetLabelsForPack(monitoringPack) + assert.Nil(t, err) + assert.Len(t, labels, 1) + assert.Equal(t, "MySQL Monitoring", labels[0].Name) + + osqueryLabel := &kolide.Label{ + Name: "Osquery Monitoring", + QueryID: osqueryRunningQuery.ID, + } + osqueryLabel, err = ds.NewLabel(osqueryLabel) + assert.Nil(t, err) + + err = ds.AddLabelToPack(osqueryLabel.ID, monitoringPack.ID) + assert.Nil(t, err) + + labels, err = ds.GetLabelsForPack(monitoringPack) + assert.Nil(t, err) + assert.Len(t, labels, 2) +} diff --git a/server/datastore/gorm.go b/server/datastore/gorm.go index 11083119c2..7180367b2a 100644 --- a/server/datastore/gorm.go +++ b/server/datastore/gorm.go @@ -29,7 +29,6 @@ var tables = [...]interface{}{ &kolide.Label{}, &kolide.LabelQueryExecution{}, &kolide.Option{}, - &kolide.Target{}, &kolide.DistributedQueryCampaign{}, &kolide.DistributedQueryCampaignTarget{}, &kolide.Query{}, @@ -301,6 +300,42 @@ func (orm gormDB) NewLabel(label *kolide.Label) (*kolide.Label, error) { return label, nil } +func (orm gormDB) SaveLabel(label *kolide.Label) error { + if label == nil { + return errors.New( + "error saving label", + "nil pointer passed to SaveLabel", + ) + } + return orm.DB.Save(label).Error +} + +func (orm gormDB) DeleteLabel(lid uint) error { + err := orm.DB.Where("id = ?", lid).Delete(&kolide.Label{}).Error + if err != nil { + return err + } + + return orm.DB.Where("target_id = ? and type = ?", lid, kolide.TargetLabel).Delete(&kolide.PackTarget{}).Error +} + +func (orm gormDB) Label(lid uint) (*kolide.Label, error) { + label := &kolide.Label{ + ID: lid, + } + err := orm.DB.Where("id = ?", label.ID).First(&label).Error + if err != nil { + return nil, err + } + return label, nil +} + +func (orm gormDB) Labels() ([]*kolide.Label, error) { + var labels []*kolide.Label + err := orm.DB.Find(&labels).Error + return labels, err +} + func (orm gormDB) LabelQueriesForHost(host *kolide.Host, cutoff time.Time) (map[string]string, error) { if host == nil { return nil, errors.New( @@ -390,21 +425,14 @@ matches = VALUES(matches) return nil } -func (orm gormDB) LabelsForHost(host *kolide.Host) ([]kolide.Label, error) { - if host == nil { - return nil, errors.New( - "error finding host queries", - "nil pointer passed to LabelQueriesForHost", - ) - } - +func (orm gormDB) LabelsForHost(hid uint) ([]kolide.Label, error) { results := []kolide.Label{} err := orm.DB.Raw(` SELECT labels.* from labels, label_query_executions lqe WHERE lqe.host_id = ? AND lqe.label_id = labels.id AND lqe.matches -`, host.ID).Scan(&results).Error +`, hid).Scan(&results).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, errors.DatabaseError(err) @@ -433,29 +461,22 @@ func (orm gormDB) SavePack(pack *kolide.Pack) error { return orm.DB.Save(pack).Error } -func (orm gormDB) DeletePack(pack *kolide.Pack) error { - if pack == nil { - return errors.New( - "error deleting pack", - "nil pointer passed to DeletePack", - ) - } - err := orm.DB.Delete(pack).Error +func (orm gormDB) DeletePack(pid uint) error { + err := orm.DB.Where("id = ?", pid).Delete(&kolide.Pack{}).Error if err != nil { return err } - err = orm.DB.Where("pack_id = ?", pack.ID).Delete(&kolide.PackQuery{}).Error + err = orm.DB.Where("pack_id = ?", pid).Delete(&kolide.PackQuery{}).Error if err != nil { return err } - - return nil + return orm.DB.Where("pack_id = ?", pid).Delete(&kolide.PackTarget{}).Error } -func (orm gormDB) Pack(id uint) (*kolide.Pack, error) { +func (orm gormDB) Pack(pid uint) (*kolide.Pack, error) { pack := &kolide.Pack{ - ID: id, + ID: pid, } err := orm.DB.Where(pack).First(pack).Error if err != nil { @@ -470,16 +491,10 @@ func (orm gormDB) Packs() ([]*kolide.Pack, error) { return packs, err } -func (orm gormDB) AddQueryToPack(query *kolide.Query, pack *kolide.Pack) error { - if query == nil || pack == nil { - return errors.New( - "error adding query from pack", - "nil pointer passed to AddQueryToPack", - ) - } +func (orm gormDB) AddQueryToPack(qid uint, pid uint) error { pq := &kolide.PackQuery{ - QueryID: query.ID, - PackID: pack.ID, + QueryID: qid, + PackID: pid, } return orm.DB.Create(pq).Error } @@ -555,3 +570,111 @@ func (orm gormDB) RemoveQueryFromPack(query *kolide.Query, pack *kolide.Pack) er } return orm.DB.Where(pq).Delete(pq).Error } + +func (orm gormDB) AddLabelToPack(lid uint, pid uint) error { + pt := &kolide.PackTarget{ + Type: kolide.TargetLabel, + PackID: pid, + TargetID: lid, + } + + return orm.DB.Create(pt).Error +} + +func (orm gormDB) ActivePacksForHost(hid uint) ([]*kolide.Pack, error) { + packs := []*kolide.Pack{} + + // we will need to give some subset of packs to this host based on the + // labels which this host is known to belong to + allPacks, err := orm.Packs() + if err != nil { + return nil, err + } + + // pull the labels that this host belongs to + labels, err := orm.LabelsForHost(hid) + if err != nil { + return nil, err + } + + // in order to use o(1) array indexing in an o(n) loop vs a o(n^2) double + // for loop iteration, we must create the array which may be indexed below + labelIDs := map[uint]bool{} + for _, label := range labels { + labelIDs[label.ID] = true + } + + for _, pack := range allPacks { + // for each pack, we must know what labels have been assigned to that + // pack + labelsForPack, err := orm.GetLabelsForPack(pack) + if err != nil { + return nil, err + } + + // o(n) iteration to determine whether or not a pack is enabled + // in this case, n is len(labelsForPack) + for _, label := range labelsForPack { + if labelIDs[label.ID] { + packs = append(packs, pack) + break + } + } + } + + return packs, nil +} + +func (orm gormDB) GetLabelsForPack(pack *kolide.Pack) ([]*kolide.Label, error) { + if pack == nil { + return nil, errors.New( + "error getting labels for pack", + "nil pointer passed to GetLabelsForPack", + ) + } + + results := []*kolide.Label{} + err := orm.DB.Raw(` +SELECT + l.id, + l.created_at, + l.updated_at, + l.name, + l.query_id +FROM + labels l +JOIN + pack_targets pt +ON + pt.target_id = l.id +WHERE + pt.type = ? + AND + pt.pack_id = ?; + +`, + kolide.TargetLabel, pack.ID).Scan(&results).Error + + if err != nil && err != gorm.ErrRecordNotFound { + return nil, errors.DatabaseError(err) + } + + return results, nil +} + +func (orm gormDB) RemoveLabelFromPack(label *kolide.Label, pack *kolide.Pack) error { + if label == nil || pack == nil { + return errors.New( + "error removing label from pack", + "nil pointer passed to RemoveLabelFromPack", + ) + } + + pt := &kolide.PackTarget{ + Type: kolide.TargetLabel, + PackID: pack.ID, + TargetID: label.ID, + } + + return orm.DB.Delete(pt).Error +} diff --git a/server/datastore/gorm_test.go b/server/datastore/gorm_test.go index cc3667879f..f734ca9ecf 100644 --- a/server/datastore/gorm_test.go +++ b/server/datastore/gorm_test.go @@ -117,3 +117,8 @@ func TestAddAndRemoveQueryFromPack(t *testing.T) { ds := setup(t) testAddAndRemoveQueryFromPack(t, ds) } + +func TestManagingLabelsOnPacks(t *testing.T) { + ds := setup(t) + testManagingLabelsOnPacks(t, ds) +} diff --git a/server/datastore/inmem_osquery.go b/server/datastore/inmem_osquery.go index e401875a7c..e44a809bbb 100644 --- a/server/datastore/inmem_osquery.go +++ b/server/datastore/inmem_osquery.go @@ -26,14 +26,14 @@ func (orm *inmem) NewLabel(label *kolide.Label) (*kolide.Label, error) { return &newLabel, nil } -func (orm *inmem) LabelsForHost(host *kolide.Host) ([]kolide.Label, error) { +func (orm *inmem) LabelsForHost(hid uint) ([]kolide.Label, error) { orm.mtx.Lock() defer orm.mtx.Unlock() // First get IDs of label executions for the host resLabels := []kolide.Label{} for _, lqe := range orm.labelQueryExecutions { - if lqe.HostID == host.ID && lqe.Matches { + if lqe.HostID == hid && lqe.Matches { if label := orm.labels[lqe.LabelID]; label != nil { resLabels = append(resLabels, *label) } diff --git a/server/kolide/datastore.go b/server/kolide/datastore.go index 382d6eb274..8f4dfbfc17 100644 --- a/server/kolide/datastore.go +++ b/server/kolide/datastore.go @@ -5,7 +5,7 @@ type Datastore interface { UserStore QueryStore PackStore - OsqueryStore + LabelStore HostStore PasswordResetStore SessionStore diff --git a/server/kolide/labels.go b/server/kolide/labels.go index c2e103affa..07bfa7bd63 100644 --- a/server/kolide/labels.go +++ b/server/kolide/labels.go @@ -1 +1,61 @@ package kolide + +import ( + "time" + + "golang.org/x/net/context" +) + +type LabelStore interface { + // Label methods + NewLabel(Label *Label) (*Label, error) + SaveLabel(Label *Label) error + DeleteLabel(lid uint) error + Label(lid uint) (*Label, error) + Labels() ([]*Label, error) + + // LabelQueriesForHost returns the label queries that should be executed + // for the given host. The cutoff is the minimum timestamp a query + // execution should have to be considered "fresh". Executions that are + // not fresh will be repeated. Results are returned in a map of label + // id -> query + LabelQueriesForHost(host *Host, cutoff time.Time) (map[string]string, error) + + // RecordLabelQueryExecutions saves the results of label queries. The + // results map is a map of label id -> whether or not the label + // matches. The time parameter is the timestamp to save with the query + // execution. + RecordLabelQueryExecutions(host *Host, results map[string]bool, t time.Time) error + + // LabelsForHost returns the labels that the given host is in. + LabelsForHost(hid uint) ([]Label, error) +} + +type LabelService interface { + GetAllLabels(ctx context.Context) ([]*Label, error) + GetLabel(ctx context.Context, id uint) (*Label, error) + NewLabel(ctx context.Context, p LabelPayload) (*Label, error) + ModifyLabel(ctx context.Context, id uint, p LabelPayload) (*Label, error) + DeleteLabel(ctx context.Context, id uint) error +} + +type LabelPayload struct { + Name *string + QueryID *uint `json:"query_id"` +} + +type Label struct { + ID uint `gorm:"primary_key"` + CreatedAt time.Time + UpdatedAt time.Time + Name string `gorm:"not null;unique_index:idx_label_unique_name"` + QueryID uint +} + +type LabelQueryExecution struct { + ID uint `gorm:"primary_key"` + UpdatedAt time.Time + Matches bool + LabelID uint // Note we manually specify a unique index on these + HostID uint // fields in gormDB.Migrate +} diff --git a/server/kolide/osquery.go b/server/kolide/osquery.go index 6c498d702c..d63dba1722 100644 --- a/server/kolide/osquery.go +++ b/server/kolide/osquery.go @@ -1,32 +1,9 @@ package kolide import ( - "time" - "golang.org/x/net/context" ) -type OsqueryStore interface { - // LabelQueriesForHost returns the label queries that should be executed - // for the given host. The cutoff is the minimum timestamp a query - // execution should have to be considered "fresh". Executions that are - // not fresh will be repeated. Results are returned in a map of label - // id -> query - LabelQueriesForHost(host *Host, cutoff time.Time) (map[string]string, error) - - // RecordLabelQueryExecutions saves the results of label queries. The - // results map is a map of label id -> whether or not the label - // matches. The time parameter is the timestamp to save with the query - // execution. - RecordLabelQueryExecutions(host *Host, results map[string]bool, t time.Time) error - - // NewLabel saves a new label. - NewLabel(label *Label) (*Label, error) - - // LabelsForHost returns the labels that the given host is in. - LabelsForHost(host *Host) ([]Label, error) -} - type OsqueryService interface { EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string) (string, error) AuthenticateHost(ctx context.Context, nodeKey string) (*Host, error) @@ -62,7 +39,7 @@ type PackContent struct { type Packs map[string]PackContent -type Options struct { +type OsqueryOptions struct { PackDelimiter string `json:"pack_delimiter,omitempty"` DisableDistributed bool `json:"disable_distributed"` } @@ -74,9 +51,9 @@ type Decorators struct { } type OsqueryConfig struct { - Options Options `json:"options,omitempty"` - Decorators Decorators `json:"decorators,omitempty"` - Packs Packs `json:"packs,omitempty"` + Options OsqueryOptions `json:"options,omitempty"` + Decorators Decorators `json:"decorators,omitempty"` + Packs Packs `json:"packs,omitempty"` } type OsqueryResultLog struct { diff --git a/server/kolide/packs.go b/server/kolide/packs.go index bbfee716aa..b607003b4c 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -10,14 +10,22 @@ type PackStore interface { // Pack methods NewPack(pack *Pack) error SavePack(pack *Pack) error - DeletePack(pack *Pack) error - Pack(id uint) (*Pack, error) + DeletePack(pid uint) error + Pack(pid uint) (*Pack, error) Packs() ([]*Pack, error) // Modifying the queries in packs - AddQueryToPack(query *Query, pack *Pack) error + AddQueryToPack(qid uint, pid uint) error GetQueriesInPack(pack *Pack) ([]*Query, error) RemoveQueryFromPack(query *Query, pack *Pack) error + + // Modifying the labels for packs + AddLabelToPack(lid uint, pid uint) error + GetLabelsForPack(pack *Pack) ([]*Label, error) + RemoveLabelFromPack(label *Label, pack *Pack) error + + // Packs from the host's perspective + ActivePacksForHost(hid uint) ([]*Pack, error) } type PackService interface { @@ -30,6 +38,10 @@ type PackService interface { AddQueryToPack(ctx context.Context, qid, pid uint) error GetQueriesInPack(ctx context.Context, id uint) ([]*Query, error) RemoveQueryFromPack(ctx context.Context, qid, pid uint) error + + AddLabelToPack(ctx context.Context, lid, pid uint) error + GetLabelsForPack(ctx context.Context, pid uint) ([]*Label, error) + RemoveLabelFromPack(ctx context.Context, lid, pid uint) error } type Pack struct { @@ -48,8 +60,16 @@ type PackQuery struct { QueryID uint } +type TargetType int + +const ( + TargetLabel TargetType = iota + TargetHost +) + type PackTarget struct { ID uint `gorm:"primary_key"` + Type TargetType PackID uint TargetID uint } diff --git a/server/kolide/queries.go b/server/kolide/queries.go index 1a61396887..fbc9f3ef23 100644 --- a/server/kolide/queries.go +++ b/server/kolide/queries.go @@ -51,38 +51,6 @@ type Query struct { Version string } -type Label struct { - ID uint `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - Name string `gorm:"not null;unique_index:idx_label_unique_name"` - QueryID uint -} - -type LabelQueryExecution struct { - ID uint `gorm:"primary_key"` - UpdatedAt time.Time - Matches bool - LabelID uint // Note we manually specify a unique index on these - HostID uint // fields in gormDB.Migrate -} - -type TargetType int - -const ( - TargetLabel TargetType = iota - TargetHost TargetType = iota -) - -type Target struct { - ID uint `gorm:"primary_key"` - CreatedAt time.Time - UpdatedAt time.Time - Type TargetType - TargetID uint - QueryID uint -} - type DistributedQueryStatus int const ( @@ -103,6 +71,7 @@ type DistributedQueryCampaign struct { type DistributedQueryCampaignTarget struct { ID uint `gorm:"primary_key"` + Type TargetType DistributedQueryCampaignID uint TargetID uint } @@ -110,10 +79,10 @@ type DistributedQueryCampaignTarget struct { type DistributedQueryExecutionStatus int const ( - ExecutionWaiting DistributedQueryExecutionStatus = iota - ExecutionRequested DistributedQueryExecutionStatus = iota - ExecutionSucceeded DistributedQueryExecutionStatus = iota - ExecutionFailed DistributedQueryExecutionStatus = iota + ExecutionWaiting DistributedQueryExecutionStatus = iota + ExecutionRequested + ExecutionSucceeded + ExecutionFailed ) type DistributedQueryExecution struct { @@ -136,9 +105,9 @@ type Option struct { type DecoratorType int const ( - DecoratorLoad DecoratorType = iota - DecoratorAlways DecoratorType = iota - DecoratorInterval DecoratorType = iota + DecoratorLoad DecoratorType = iota + DecoratorAlways + DecoratorInterval ) type Decorator struct { diff --git a/server/kolide/service.go b/server/kolide/service.go index 237de77444..0700c07071 100644 --- a/server/kolide/service.go +++ b/server/kolide/service.go @@ -5,6 +5,7 @@ type Service interface { UserService SessionService PackService + LabelService QueryService OsqueryService HostService diff --git a/server/service/endpoint_labels.go b/server/service/endpoint_labels.go new file mode 100644 index 0000000000..98bac6e345 --- /dev/null +++ b/server/service/endpoint_labels.go @@ -0,0 +1,158 @@ +package service + +import ( + "github.com/go-kit/kit/endpoint" + "github.com/kolide/kolide-ose/server/kolide" + "golang.org/x/net/context" +) + +//////////////////////////////////////////////////////////////////////////////// +// Get Label +//////////////////////////////////////////////////////////////////////////////// + +type getLabelRequest struct { + ID uint +} + +type getLabelResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + QueryID uint `json:"query_id"` + Err error `json:"error,omitempty"` +} + +func (r getLabelResponse) error() error { return r.Err } + +func makeGetLabelEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getLabelRequest) + label, err := svc.GetLabel(ctx, req.ID) + if err != nil { + return getLabelResponse{Err: err}, nil + } + return getLabelResponse{ + ID: label.ID, + Name: label.Name, + QueryID: label.QueryID, + }, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get All Labels +//////////////////////////////////////////////////////////////////////////////// + +type getAllLabelsResponse struct { + Labels []getLabelResponse `json:"labels"` + Err error `json:"error,omitempty"` +} + +func (r getAllLabelsResponse) error() error { return r.Err } + +func makeGetAllLabelsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + labels, err := svc.GetAllLabels(ctx) + if err != nil { + return getAllLabelsResponse{Err: err}, nil + } + var resp getAllLabelsResponse + for _, label := range labels { + resp.Labels = append(resp.Labels, getLabelResponse{ + ID: label.ID, + Name: label.Name, + QueryID: label.QueryID, + }) + } + return resp, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Create Label +//////////////////////////////////////////////////////////////////////////////// + +type createLabelRequest struct { + payload kolide.LabelPayload +} + +type createLabelResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + QueryID uint `json:"query_id"` + Err error `json:"error,omitempty"` +} + +func (r createLabelResponse) error() error { return r.Err } + +func makeCreateLabelEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(createLabelRequest) + label, err := svc.NewLabel(ctx, req.payload) + if err != nil { + return createLabelResponse{Err: err}, nil + } + return createLabelResponse{ + ID: label.ID, + Name: label.Name, + QueryID: label.QueryID, + }, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Modify Label +//////////////////////////////////////////////////////////////////////////////// + +type modifyLabelRequest struct { + ID uint + payload kolide.LabelPayload +} + +type modifyLabelResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + QueryID uint `json:"query_id"` + Err error `json:"error,omitempty"` +} + +func (r modifyLabelResponse) error() error { return r.Err } + +func makeModifyLabelEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(modifyLabelRequest) + label, err := svc.ModifyLabel(ctx, req.ID, req.payload) + if err != nil { + return modifyLabelResponse{Err: err}, nil + } + return modifyLabelResponse{ + ID: label.ID, + Name: label.Name, + QueryID: label.QueryID, + }, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Delete Label +//////////////////////////////////////////////////////////////////////////////// + +type deleteLabelRequest struct { + ID uint +} + +type deleteLabelResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteLabelResponse) error() error { return r.Err } + +func makeDeleteLabelEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteLabelRequest) + err := svc.DeleteLabel(ctx, req.ID) + if err != nil { + return deleteLabelResponse{Err: err}, nil + } + return deleteLabelResponse{}, nil + } +} diff --git a/server/service/endpoint_packs.go b/server/service/endpoint_packs.go index 3ef3e043bd..9c0d4f2657 100644 --- a/server/service/endpoint_packs.go +++ b/server/service/endpoint_packs.go @@ -248,3 +248,90 @@ func makeDeleteQueryFromPackEndpoint(svc kolide.Service) endpoint.Endpoint { return deleteQueryFromPackResponse{}, nil } } + +//////////////////////////////////////////////////////////////////////////////// +// Add Label To Pack +//////////////////////////////////////////////////////////////////////////////// + +type addLabelToPackRequest struct { + PackID uint + LabelID uint +} + +type addLabelToPackResponse struct { + Err error `json:"error,omitempty"` +} + +func (r addLabelToPackResponse) error() error { return r.Err } + +func makeAddLabelToPackEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(addLabelToPackRequest) + err := svc.AddLabelToPack(ctx, req.LabelID, req.PackID) + if err != nil { + return addLabelToPackResponse{Err: err}, nil + } + return addLabelToPackResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Labels For Pack +//////////////////////////////////////////////////////////////////////////////// + +type getLabelsForPackRequest struct { + PackID uint +} + +type getLabelsForPackResponse struct { + Labels []getLabelResponse + Err error `json:"error,omitempty"` +} + +func (r getLabelsForPackResponse) error() error { return r.Err } + +func makeGetLabelsForPackEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(getLabelsForPackRequest) + labels, err := svc.GetLabelsForPack(ctx, req.PackID) + if err != nil { + return getLabelsForPackResponse{Err: err}, nil + } + + var resp getLabelsForPackResponse + for _, label := range labels { + resp.Labels = append(resp.Labels, getLabelResponse{ + ID: label.ID, + Name: label.Name, + QueryID: label.QueryID, + }) + } + return resp, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Delete Label From Pack +//////////////////////////////////////////////////////////////////////////////// + +type deleteLabelFromPackRequest struct { + LabelID uint + PackID uint +} + +type deleteLabelFromPackResponse struct { + Err error `json:"error,omitempty"` +} + +func (r deleteLabelFromPackResponse) error() error { return r.Err } + +func makeDeleteLabelFromPackEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(deleteLabelFromPackRequest) + err := svc.RemoveLabelFromPack(ctx, req.LabelID, req.PackID) + if err != nil { + return deleteLabelFromPackResponse{Err: err}, nil + } + return deleteLabelFromPackResponse{}, nil + } +} diff --git a/server/service/handler.go b/server/service/handler.go index fa910eda97..d574fe1d27 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -49,6 +49,14 @@ type KolideEndpoints struct { GetDistributedQueries endpoint.Endpoint SubmitDistributedQueryResults endpoint.Endpoint SubmitLogs endpoint.Endpoint + GetLabel endpoint.Endpoint + GetAllLabels endpoint.Endpoint + CreateLabel endpoint.Endpoint + ModifyLabel endpoint.Endpoint + DeleteLabel endpoint.Endpoint + AddLabelToPack endpoint.Endpoint + GetLabelsForPack endpoint.Endpoint + DeleteLabelFromPack endpoint.Endpoint } // MakeKolideServerEndpoints creates the Kolide API endpoints. @@ -94,6 +102,14 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint GetDistributedQueries: authenticatedHost(svc, makeGetDistributedQueriesEndpoint(svc)), SubmitDistributedQueryResults: authenticatedHost(svc, makeSubmitDistributedQueryResultsEndpoint(svc)), SubmitLogs: authenticatedHost(svc, makeSubmitLogsEndpoint(svc)), + GetLabel: authenticatedUser(jwtKey, svc, makeGetLabelEndpoint(svc)), + GetAllLabels: authenticatedUser(jwtKey, svc, makeGetAllLabelsEndpoint(svc)), + CreateLabel: authenticatedUser(jwtKey, svc, makeCreateLabelEndpoint(svc)), + ModifyLabel: authenticatedUser(jwtKey, svc, makeModifyLabelEndpoint(svc)), + DeleteLabel: authenticatedUser(jwtKey, svc, makeDeleteLabelEndpoint(svc)), + AddLabelToPack: authenticatedUser(jwtKey, svc, makeAddLabelToPackEndpoint(svc)), + GetLabelsForPack: authenticatedUser(jwtKey, svc, makeGetLabelsForPackEndpoint(svc)), + DeleteLabelFromPack: authenticatedUser(jwtKey, svc, makeDeleteLabelFromPackEndpoint(svc)), } } @@ -134,6 +150,14 @@ type kolideHandlers struct { GetDistributedQueries *kithttp.Server SubmitDistributedQueryResults *kithttp.Server SubmitLogs *kithttp.Server + GetLabel *kithttp.Server + GetAllLabels *kithttp.Server + CreateLabel *kithttp.Server + ModifyLabel *kithttp.Server + DeleteLabel *kithttp.Server + AddLabelToPack *kithttp.Server + GetLabelsForPack *kithttp.Server + DeleteLabelFromPack *kithttp.Server } func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithttp.ServerOption) kolideHandlers { @@ -177,6 +201,14 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt GetDistributedQueries: newServer(e.GetDistributedQueries, decodeGetDistributedQueriesRequest), SubmitDistributedQueryResults: newServer(e.SubmitDistributedQueryResults, decodeSubmitDistributedQueryResultsRequest), SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), + GetLabel: newServer(e.GetLabel, decodeGetLabelRequest), + GetAllLabels: newServer(e.GetAllLabels, decodeNoParamsRequest), + CreateLabel: newServer(e.CreateLabel, decodeCreateLabelRequest), + ModifyLabel: newServer(e.ModifyLabel, decodeModifyLabelRequest), + DeleteLabel: newServer(e.DeleteLabel, decodeDeleteLabelRequest), + AddLabelToPack: newServer(e.AddLabelToPack, decodeAddLabelToPackRequest), + GetLabelsForPack: newServer(e.GetLabelsForPack, decodeGetLabelsForPackRequest), + DeleteLabelFromPack: newServer(e.DeleteLabelFromPack, decodeDeleteLabelFromPackRequest), } } @@ -239,6 +271,14 @@ func attachKolideAPIRoutes(r *mux.Router, h kolideHandlers) { r.Handle("/api/v1/kolide/packs/{pid}/queries/{qid}", h.AddQueryToPack).Methods("POST") r.Handle("/api/v1/kolide/packs/{id}/queries", h.GetQueriesInPack).Methods("GET") r.Handle("/api/v1/kolide/packs/{pid}/queries/{qid}", h.DeleteQueryFromPack).Methods("DELETE") + r.Handle("/api/v1/kolide/labels/{id}", h.GetLabel).Methods("GET") + r.Handle("/api/v1/kolide/labels", h.GetAllLabels).Methods("GET") + r.Handle("/api/v1/kolide/labels", h.CreateLabel).Methods("POST") + r.Handle("/api/v1/kolide/labels/{id}", h.ModifyLabel).Methods("PATCH") + r.Handle("/api/v1/kolide/labels/{id}", h.DeleteLabel).Methods("DELETE") + r.Handle("/api/v1/kolide/packs/{pid}/labels/{lid}", h.AddLabelToPack).Methods("POST") + r.Handle("/api/v1/kolide/packs/{pid}/labels", h.GetLabelsForPack).Methods("GET") + r.Handle("/api/v1/kolide/packs/{pid}/labels/{lid}", h.DeleteLabelFromPack).Methods("DELETE") r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST") r.Handle("/api/v1/osquery/config", h.GetClientConfig).Methods("POST") diff --git a/server/service/handler_test.go b/server/service/handler_test.go index 30c73ef02c..f5af2fa01b 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -155,6 +155,38 @@ func TestAPIRoutes(t *testing.T) { verb: "POST", uri: "/api/v1/osquery/log", }, + { + verb: "GET", + uri: "/api/v1/kolide/labels/1", + }, + { + verb: "GET", + uri: "/api/v1/kolide/labels", + }, + { + verb: "POST", + uri: "/api/v1/kolide/labels", + }, + { + verb: "PATCH", + uri: "/api/v1/kolide/labels/1", + }, + { + verb: "DELETE", + uri: "/api/v1/kolide/labels/1", + }, + { + verb: "POST", + uri: "/api/v1/kolide/packs/1/labels/2", + }, + { + verb: "GET", + uri: "/api/v1/kolide/packs/1/labels", + }, + { + verb: "DELETE", + uri: "/api/v1/kolide/packs/1/labels/2", + }, } for _, route := range routes { diff --git a/server/service/service_labels.go b/server/service/service_labels.go new file mode 100644 index 0000000000..ab79905bfd --- /dev/null +++ b/server/service/service_labels.go @@ -0,0 +1,59 @@ +package service + +import ( + "github.com/kolide/kolide-ose/server/kolide" + "golang.org/x/net/context" +) + +func (svc service) GetAllLabels(ctx context.Context) ([]*kolide.Label, error) { + return svc.ds.Labels() +} + +func (svc service) GetLabel(ctx context.Context, id uint) (*kolide.Label, error) { + return svc.ds.Label(id) +} + +func (svc service) NewLabel(ctx context.Context, p kolide.LabelPayload) (*kolide.Label, error) { + label := &kolide.Label{} + + if p.Name == nil { + return nil, newInvalidArgumentError("name", "missing required argument") + } + label.Name = *p.Name + + if p.QueryID != nil { + label.QueryID = *p.QueryID + } + + label, err := svc.ds.NewLabel(label) + if err != nil { + return nil, err + } + return label, nil +} + +func (svc service) ModifyLabel(ctx context.Context, id uint, p kolide.LabelPayload) (*kolide.Label, error) { + label, err := svc.ds.Label(id) + if err != nil { + return nil, err + } + + if p.Name != nil { + label.Name = *p.Name + } + + if p.QueryID != nil { + label.QueryID = *p.QueryID + } + + err = svc.ds.SaveLabel(label) + if err != nil { + return nil, err + } + + return label, nil +} + +func (svc service) DeleteLabel(ctx context.Context, id uint) error { + return svc.ds.DeleteLabel(id) +} diff --git a/server/service/service_labels_test.go b/server/service/service_labels_test.go new file mode 100644 index 0000000000..f1804754cf --- /dev/null +++ b/server/service/service_labels_test.go @@ -0,0 +1,133 @@ +package service + +import ( + "testing" + + "github.com/kolide/kolide-ose/server/datastore" + "github.com/kolide/kolide-ose/server/kolide" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" +) + +func TestGetAllLabels(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + svc, err := newTestService(ds) + assert.Nil(t, err) + + ctx := context.Background() + + labels, err := svc.GetAllLabels(ctx) + assert.Nil(t, err) + assert.Len(t, labels, 0) + + _, err = ds.NewLabel(&kolide.Label{ + Name: "foo", + QueryID: 1, + }) + assert.Nil(t, err) + + labels, err = svc.GetAllLabels(ctx) + assert.Nil(t, err) + assert.Len(t, labels, 1) + assert.Equal(t, "foo", labels[0].Name) +} + +func TestGetLabel(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + svc, err := newTestService(ds) + assert.Nil(t, err) + + ctx := context.Background() + + label := &kolide.Label{ + Name: "foo", + QueryID: 1, + } + label, err = ds.NewLabel(label) + assert.Nil(t, err) + assert.NotZero(t, label.ID) + + labelVerify, err := svc.GetLabel(ctx, label.ID) + assert.Nil(t, err) + assert.Equal(t, label.ID, labelVerify.ID) +} + +func TestNewLabel(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + svc, err := newTestService(ds) + assert.Nil(t, err) + + ctx := context.Background() + + name := "foo" + queryID := uint(1) + label, err := svc.NewLabel(ctx, kolide.LabelPayload{ + Name: &name, + QueryID: &queryID, + }) + assert.NotZero(t, label.ID) + + assert.Nil(t, err) + + labels, err := ds.Labels() + assert.Nil(t, err) + assert.Len(t, labels, 1) + assert.Equal(t, "foo", labels[0].Name) +} + +func TestModifyLabel(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + svc, err := newTestService(ds) + assert.Nil(t, err) + + ctx := context.Background() + + label := &kolide.Label{ + Name: "foo", + QueryID: 1, + } + label, err = ds.NewLabel(label) + assert.Nil(t, err) + assert.NotZero(t, label.ID) + + newName := "bar" + labelVerify, err := svc.ModifyLabel(ctx, label.ID, kolide.LabelPayload{ + Name: &newName, + }) + assert.Nil(t, err) + assert.Equal(t, label.ID, labelVerify.ID) + assert.Equal(t, "bar", labelVerify.Name) +} + +func TestDeleteLabel(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + svc, err := newTestService(ds) + assert.Nil(t, err) + + ctx := context.Background() + + label := &kolide.Label{ + Name: "foo", + QueryID: 1, + } + label, err = ds.NewLabel(label) + assert.Nil(t, err) + assert.NotZero(t, label.ID) + + err = svc.DeleteLabel(ctx, label.ID) + assert.Nil(t, err) + + labels, err := ds.Labels() + assert.Nil(t, err) + assert.Len(t, labels, 0) +} diff --git a/server/service/service_osquery.go b/server/service/service_osquery.go index 377f0e6766..53fd0a351d 100644 --- a/server/service/service_osquery.go +++ b/server/service/service_osquery.go @@ -54,8 +54,53 @@ func (svc service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier } func (svc service) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, error) { - var config kolide.OsqueryConfig - return &config, nil + host, ok := hostctx.FromContext(ctx) + if !ok { + return nil, osqueryError{message: "internal error: missing host from request context"} + } + + config := &kolide.OsqueryConfig{ + Options: kolide.OsqueryOptions{ + PackDelimiter: "/", + DisableDistributed: false, + }, + Packs: kolide.Packs{}, + } + + packs, err := svc.ds.ActivePacksForHost(host.ID) + if err != nil { + return nil, osqueryError{message: "database error: " + err.Error()} + } + + for _, pack := range packs { + // first, we must figure out what queries are in this pack + queries, err := svc.ds.GetQueriesInPack(pack) + if err != nil { + return nil, osqueryError{message: "database error: " + err.Error()} + } + + // the serializable osquery config struct expects content in a + // particular format, so we do the conversion here + configQueries := kolide.Queries{} + for _, query := range queries { + configQueries[query.Name] = kolide.QueryContent{ + Query: query.Query, + Interval: query.Interval, + Platform: query.Platform, + Version: query.Version, + Snapshot: query.Snapshot, + } + } + + // finally, we add the pack to the client config struct with all of + // the packs queries + config.Packs[pack.Name] = kolide.PackContent{ + Platform: pack.Platform, + Queries: configQueries, + } + } + + return config, nil } func (svc service) SubmitStatusLogs(ctx context.Context, logs []kolide.OsqueryStatusLog) error { diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index 013b412794..ef31b077c2 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -377,3 +377,87 @@ func TestGetDistributedQueries(t *testing.T) { assert.Nil(t, err) assert.Equal(t, expectQueries, queries) } + +func TestGetClientConfig(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", ":memory:") + assert.Nil(t, err) + + mockClock := clock.NewMockClock() + + svc, err := newTestServiceWithClock(ds, mockClock) + assert.Nil(t, err) + + ctx := context.Background() + + hosts, err := ds.Hosts() + require.Nil(t, err) + require.Len(t, hosts, 0) + + _, err = svc.EnrollAgent(ctx, "", "user.local") + assert.Nil(t, err) + + hosts, err = ds.Hosts() + require.Nil(t, err) + require.Len(t, hosts, 1) + host := hosts[0] + + ctx = hostctx.NewContext(ctx, *host) + + // with no queries, packs, labels, etc. verify the state of a fresh host + // asking for a config + config, err := svc.GetClientConfig(ctx) + require.Nil(t, err) + assert.NotNil(t, config) + assert.False(t, config.Options.DisableDistributed) + assert.Equal(t, "/", config.Options.PackDelimiter) + + // this will be greater than 0 if we ever start inserting an administration + // pack + assert.Len(t, config.Packs, 0) + + // let's populate the database with some info + + mysqlQuery := &kolide.Query{ + Name: "MySQL", + Query: "select pid from processes where name = 'mysqld';", + } + mysqlQuery, err = ds.NewQuery(mysqlQuery) + assert.Nil(t, err) + + infoQuery := &kolide.Query{ + Name: "Info", + Query: "select * from osquery_info;", + Interval: 60, + } + infoQuery, err = ds.NewQuery(infoQuery) + assert.Nil(t, err) + + monitoringPack := &kolide.Pack{ + Name: "monitoring", + } + err = ds.NewPack(monitoringPack) + assert.Nil(t, err) + + err = ds.AddQueryToPack(infoQuery.ID, monitoringPack.ID) + assert.Nil(t, err) + + mysqlLabel := &kolide.Label{ + Name: "MySQL Monitoring", + QueryID: mysqlQuery.ID, + } + mysqlLabel, err = ds.NewLabel(mysqlLabel) + assert.Nil(t, err) + + err = ds.AddLabelToPack(mysqlLabel.ID, monitoringPack.ID) + assert.Nil(t, err) + + err = ds.RecordLabelQueryExecutions(host, map[string]bool{fmt.Sprintf("%d", mysqlQuery.ID): true}, mockClock.Now()) + assert.Nil(t, err) + + // with a minimal setup of packs, labels, and queries, will our host get the + // pack + config, err = svc.GetClientConfig(ctx) + require.Nil(t, err) + assert.Len(t, config.Packs, 1) + assert.Len(t, config.Packs["monitoring"].Queries, 1) +} diff --git a/server/service/service_packs.go b/server/service/service_packs.go index 462f2c0f2a..08fbe8d835 100644 --- a/server/service/service_packs.go +++ b/server/service/service_packs.go @@ -54,36 +54,11 @@ func (svc service) ModifyPack(ctx context.Context, id uint, p kolide.PackPayload } func (svc service) DeletePack(ctx context.Context, id uint) error { - pack, err := svc.ds.Pack(id) - if err != nil { - return err - } - - err = svc.ds.DeletePack(pack) - if err != nil { - return err - } - - return nil + return svc.ds.DeletePack(id) } func (svc service) AddQueryToPack(ctx context.Context, qid, pid uint) error { - pack, err := svc.ds.Pack(pid) - if err != nil { - return err - } - - query, err := svc.ds.Query(qid) - if err != nil { - return err - } - - err = svc.ds.AddQueryToPack(query, pack) - if err != nil { - return err - } - - return nil + return svc.ds.AddQueryToPack(qid, pid) } func (svc service) GetQueriesInPack(ctx context.Context, id uint) ([]*kolide.Query, error) { @@ -118,3 +93,39 @@ func (svc service) RemoveQueryFromPack(ctx context.Context, qid, pid uint) error return nil } +func (svc service) AddLabelToPack(ctx context.Context, lid, pid uint) error { + return svc.ds.AddLabelToPack(lid, pid) +} + +func (svc service) GetLabelsForPack(ctx context.Context, pid uint) ([]*kolide.Label, error) { + pack, err := svc.ds.Pack(pid) + if err != nil { + return nil, err + } + + labels, err := svc.ds.GetLabelsForPack(pack) + if err != nil { + return nil, err + } + + return labels, nil +} + +func (svc service) RemoveLabelFromPack(ctx context.Context, lid, pid uint) error { + pack, err := svc.ds.Pack(pid) + if err != nil { + return err + } + + label, err := svc.ds.Label(lid) + if err != nil { + return err + } + + err = svc.ds.RemoveLabelFromPack(label, pack) + if err != nil { + return err + } + + return nil +} diff --git a/server/service/service_packs_test.go b/server/service/service_packs_test.go index e87f21f68a..d386ee4bce 100644 --- a/server/service/service_packs_test.go +++ b/server/service/service_packs_test.go @@ -186,7 +186,7 @@ func TestGetQueriesInPack(t *testing.T) { assert.Nil(t, err) assert.NotZero(t, query.ID) - err = ds.AddQueryToPack(query, pack) + err = ds.AddQueryToPack(query.ID, pack.ID) assert.Nil(t, err) queries, err := svc.GetQueriesInPack(ctx, pack.ID) @@ -218,7 +218,7 @@ func TestRemoveQueryFromPack(t *testing.T) { assert.Nil(t, err) assert.NotZero(t, query.ID) - err = ds.AddQueryToPack(query, pack) + err = ds.AddQueryToPack(query.ID, pack.ID) assert.Nil(t, err) queries, err := ds.GetQueriesInPack(pack) diff --git a/server/service/transport_labels.go b/server/service/transport_labels.go new file mode 100644 index 0000000000..9a1df7321b --- /dev/null +++ b/server/service/transport_labels.go @@ -0,0 +1,49 @@ +package service + +import ( + "encoding/json" + "net/http" + + "golang.org/x/net/context" +) + +func decodeCreateLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req createLabelRequest + if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { + return nil, err + } + return req, nil +} + +func decodeModifyLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req modifyLabelRequest + if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil { + return nil, err + } + req.ID = id + return req, nil +} + +func decodeDeleteLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req deleteLabelRequest + req.ID = id + return req, nil +} + +func decodeGetLabelRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req getLabelRequest + req.ID = id + return req, nil +} diff --git a/server/service/transport_labels_test.go b/server/service/transport_labels_test.go new file mode 100644 index 0000000000..afe11ca49b --- /dev/null +++ b/server/service/transport_labels_test.go @@ -0,0 +1,90 @@ +package service + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + + "golang.org/x/net/context" +) + +func TestDecodeCreateLabelRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/kolide/labels", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeCreateLabelRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(createLabelRequest) + assert.Equal(t, "foo", *params.payload.Name) + assert.Equal(t, uint(4), *params.payload.QueryID) + }).Methods("POST") + + var body bytes.Buffer + body.Write([]byte(`{ + "name": "foo", + "query_id": 4 + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("POST", "/api/v1/kolide/labels", &body), + ) +} + +func TestDecodeModifyLabelRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/kolide/labels/{id}", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeModifyLabelRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(modifyLabelRequest) + assert.Equal(t, "foo", *params.payload.Name) + assert.Equal(t, uint(1), params.ID) + }).Methods("PATCH") + + var body bytes.Buffer + body.Write([]byte(`{ + "name": "foo" + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("PATCH", "/api/v1/kolide/labels/1", &body), + ) +} + +func TestDecodeDeleteLabelRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/kolide/labels/{id}", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeDeleteLabelRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(deleteLabelRequest) + assert.Equal(t, uint(1), params.ID) + }).Methods("DELETE") + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("DELETE", "/api/v1/kolide/labels/1", nil), + ) +} + +func TestDecodeGetLabelRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/kolide/labels/{id}", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeGetLabelRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(getLabelRequest) + assert.Equal(t, uint(1), params.ID) + }).Methods("GET") + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("GET", "/api/v1/kolide/labels/1", nil), + ) +} diff --git a/server/service/transport_packs.go b/server/service/transport_packs.go index 5d82f700a8..494806f166 100644 --- a/server/service/transport_packs.go +++ b/server/service/transport_packs.go @@ -88,3 +88,43 @@ func decodeDeleteQueryFromPackRequest(ctx context.Context, r *http.Request) (int req.QueryID = qid return req, nil } + +func decodeAddLabelToPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { + lid, err := idFromRequest(r, "lid") + if err != nil { + return nil, err + } + pid, err := idFromRequest(r, "pid") + if err != nil { + return nil, err + } + return addLabelToPackRequest{ + PackID: pid, + LabelID: lid, + }, nil +} + +func decodeGetLabelsForPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { + pid, err := idFromRequest(r, "pid") + if err != nil { + return nil, err + } + var req getLabelsForPackRequest + req.PackID = pid + return req, nil +} + +func decodeDeleteLabelFromPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { + lid, err := idFromRequest(r, "lid") + if err != nil { + return nil, err + } + pid, err := idFromRequest(r, "pid") + if err != nil { + return nil, err + } + var req deleteLabelFromPackRequest + req.PackID = pid + req.LabelID = lid + return req, nil +}