Osquery Configuration Control (#244)

Label management APIs and an osquery config endpoint based on active pack and label state.
This commit is contained in:
Mike Arpaia 2016-10-02 20:14:35 -07:00 committed by GitHub
parent 6d1c963bfa
commit a03347489c
23 changed files with 1176 additions and 142 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ type Datastore interface {
UserStore
QueryStore
PackStore
OsqueryStore
LabelStore
HostStore
PasswordResetStore
SessionStore

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ type Service interface {
UserService
SessionService
PackService
LabelService
QueryService
OsqueryService
HostService

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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