diff --git a/server/datastore/gorm.go b/server/datastore/gorm.go index b7d300682c..1604fb6cdc 100644 --- a/server/datastore/gorm.go +++ b/server/datastore/gorm.go @@ -84,3 +84,15 @@ func openGORM(driver, conn string, maxAttempts int) (*gorm.DB, error) { } return db, nil } + +// applyLimitOffset applies the appropriate limit and offset parameters to the +// gorm.DB instance, returning a DB that can be chained as usual with *gorm.DB. +func (orm *gormDB) applyListOptions(opt kolide.ListOptions) *gorm.DB { + if opt.PerPage == 0 { + // PerPage value of 0 indicates unlimited + return orm.DB + } + + offset := opt.Page * opt.PerPage + return orm.DB.Limit(opt.PerPage).Offset(offset) +} diff --git a/server/datastore/gorm_hosts.go b/server/datastore/gorm_hosts.go index 733c117d6d..23cc9f50b9 100644 --- a/server/datastore/gorm_hosts.go +++ b/server/datastore/gorm_hosts.go @@ -100,9 +100,9 @@ func (orm gormDB) Host(id uint) (*kolide.Host, error) { return host, nil } -func (orm gormDB) Hosts() ([]*kolide.Host, error) { +func (orm gormDB) Hosts(opt kolide.ListOptions) ([]*kolide.Host, error) { var hosts []*kolide.Host - err := orm.DB.Find(&hosts).Error + err := orm.applyListOptions(opt).Find(&hosts).Error if err != nil { return nil, err } diff --git a/server/datastore/gorm_invite.go b/server/datastore/gorm_invite.go index 1830d319cd..2914034c51 100644 --- a/server/datastore/gorm_invite.go +++ b/server/datastore/gorm_invite.go @@ -21,9 +21,9 @@ func (orm gormDB) InviteByEmail(email string) (*kolide.Invite, error) { return invite, nil } -func (orm gormDB) Invites() ([]*kolide.Invite, error) { +func (orm gormDB) Invites(opt kolide.ListOptions) ([]*kolide.Invite, error) { var invites []*kolide.Invite - err := orm.DB.Find(&invites).Error + err := orm.applyListOptions(opt).Find(&invites).Error if err != nil { return nil, err } diff --git a/server/datastore/gorm_labels.go b/server/datastore/gorm_labels.go index 55bd971d3f..a2b921501f 100644 --- a/server/datastore/gorm_labels.go +++ b/server/datastore/gorm_labels.go @@ -54,9 +54,9 @@ func (orm gormDB) Label(lid uint) (*kolide.Label, error) { return label, nil } -func (orm gormDB) Labels() ([]*kolide.Label, error) { +func (orm gormDB) Labels(opt kolide.ListOptions) ([]*kolide.Label, error) { var labels []*kolide.Label - err := orm.DB.Find(&labels).Error + err := orm.applyListOptions(opt).Find(&labels).Error return labels, err } diff --git a/server/datastore/gorm_packs.go b/server/datastore/gorm_packs.go index ac5acb4ea5..27543672d7 100644 --- a/server/datastore/gorm_packs.go +++ b/server/datastore/gorm_packs.go @@ -50,9 +50,9 @@ func (orm gormDB) Pack(pid uint) (*kolide.Pack, error) { return pack, nil } -func (orm gormDB) Packs() ([]*kolide.Pack, error) { +func (orm gormDB) Packs(opt kolide.ListOptions) ([]*kolide.Pack, error) { var packs []*kolide.Pack - err := orm.DB.Find(&packs).Error + err := orm.applyListOptions(opt).Find(&packs).Error return packs, err } @@ -151,7 +151,7 @@ func (orm gormDB) ActivePacksForHost(hid uint) ([]*kolide.Pack, error) { // 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() + allPacks, err := orm.Packs(kolide.ListOptions{}) if err != nil { return nil, err } diff --git a/server/datastore/gorm_queries.go b/server/datastore/gorm_queries.go index 4ff80ef889..08ddbe5600 100644 --- a/server/datastore/gorm_queries.go +++ b/server/datastore/gorm_queries.go @@ -50,8 +50,8 @@ func (orm gormDB) Query(id uint) (*kolide.Query, error) { return query, nil } -func (orm gormDB) Queries() ([]*kolide.Query, error) { +func (orm gormDB) Queries(opt kolide.ListOptions) ([]*kolide.Query, error) { var queries []*kolide.Query - err := orm.DB.Find(&queries).Error + err := orm.applyListOptions(opt).Find(&queries).Error return queries, err } diff --git a/server/datastore/gorm_users.go b/server/datastore/gorm_users.go index 4676dc3367..a2fd4374be 100644 --- a/server/datastore/gorm_users.go +++ b/server/datastore/gorm_users.go @@ -23,9 +23,9 @@ func (orm gormDB) User(username string) (*kolide.User, error) { return user, nil } -func (orm gormDB) Users() ([]*kolide.User, error) { +func (orm gormDB) Users(opt kolide.ListOptions) ([]*kolide.User, error) { var users []*kolide.User - err := orm.DB.Find(&users).Error + err := orm.applyListOptions(opt).Find(&users).Error if err != nil { return nil, err } diff --git a/server/datastore/inmem.go b/server/datastore/inmem.go index f1abe18504..3c1c9bf84b 100644 --- a/server/datastore/inmem.go +++ b/server/datastore/inmem.go @@ -44,3 +44,23 @@ func (orm *inmem) Migrate() error { func (orm *inmem) Drop() error { return orm.Migrate() } + +// getLimitOffsetSliceBounds returns the bounds that should be used for +// re-slicing the results to comply with the requested ListOptions. Lack of +// generics forces us to do this rather than reslicing in this method. +func (orm *inmem) getLimitOffsetSliceBounds(opt kolide.ListOptions, length int) (low uint, high uint) { + if opt.PerPage == 0 { + // PerPage value of 0 indicates unlimited + return 0, uint(length) + } + + offset := opt.Page * opt.PerPage + max := offset + opt.PerPage + if offset > uint(length) { + offset = uint(length) + } + if max > uint(length) { + max = uint(length) + } + return offset, max +} diff --git a/server/datastore/inmem_hosts.go b/server/datastore/inmem_hosts.go index e8258804a8..a0244f1e15 100644 --- a/server/datastore/inmem_hosts.go +++ b/server/datastore/inmem_hosts.go @@ -2,6 +2,7 @@ package datastore import ( "errors" + "sort" "time" "github.com/kolide/kolide-ose/server/kolide" @@ -59,14 +60,25 @@ func (orm *inmem) Host(id uint) (*kolide.Host, error) { return host, nil } -func (orm *inmem) Hosts() ([]*kolide.Host, error) { +func (orm *inmem) Hosts(opt kolide.ListOptions) ([]*kolide.Host, error) { orm.mtx.Lock() defer orm.mtx.Unlock() - hosts := []*kolide.Host{} - for _, host := range orm.hosts { - hosts = append(hosts, host) + // We need to sort by keys to provide reliable ordering + keys := []int{} + for k, _ := range orm.hosts { + keys = append(keys, int(k)) } + sort.Ints(keys) + + hosts := []*kolide.Host{} + for _, k := range keys { + hosts = append(hosts, orm.hosts[uint(k)]) + } + + // Apply limit/offset + low, high := orm.getLimitOffsetSliceBounds(opt, len(hosts)) + hosts = hosts[low:high] return hosts, nil } diff --git a/server/datastore/inmem_invite.go b/server/datastore/inmem_invite.go index bd99fe1f79..31606bd678 100644 --- a/server/datastore/inmem_invite.go +++ b/server/datastore/inmem_invite.go @@ -1,6 +1,10 @@ package datastore -import "github.com/kolide/kolide-ose/server/kolide" +import ( + "sort" + + "github.com/kolide/kolide-ose/server/kolide" +) // NewInvite creates and stores a new invitation in a DB. func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) { @@ -19,14 +23,25 @@ func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) { } // Invites lists all invites in the datastore. -func (orm *inmem) Invites() ([]*kolide.Invite, error) { +func (orm *inmem) Invites(opt kolide.ListOptions) ([]*kolide.Invite, error) { orm.mtx.Lock() defer orm.mtx.Unlock() - var invites []*kolide.Invite - for _, invite := range orm.invites { - invites = append(invites, invite) + // We need to sort by keys to provide reliable ordering + keys := []int{} + for k, _ := range orm.invites { + keys = append(keys, int(k)) } + sort.Ints(keys) + + invites := []*kolide.Invite{} + for _, k := range keys { + invites = append(invites, orm.invites[uint(k)]) + } + + // Apply limit/offset + low, high := orm.getLimitOffsetSliceBounds(opt, len(invites)) + invites = invites[low:high] return invites, nil } diff --git a/server/datastore/inmem_packs.go b/server/datastore/inmem_packs.go index 65ea47fb07..6a3035bb27 100644 --- a/server/datastore/inmem_packs.go +++ b/server/datastore/inmem_packs.go @@ -1,6 +1,8 @@ package datastore import ( + "sort" + "github.com/kolide/kolide-ose/server/kolide" ) @@ -58,14 +60,25 @@ func (orm *inmem) Pack(id uint) (*kolide.Pack, error) { return pack, nil } -func (orm *inmem) Packs() ([]*kolide.Pack, error) { +func (orm *inmem) Packs(opt kolide.ListOptions) ([]*kolide.Pack, error) { orm.mtx.Lock() defer orm.mtx.Unlock() - packs := []*kolide.Pack{} - for _, pack := range orm.packs { - packs = append(packs, pack) + // We need to sort by keys to provide reliable ordering + keys := []int{} + for k, _ := range orm.packs { + keys = append(keys, int(k)) } + sort.Ints(keys) + + packs := []*kolide.Pack{} + for _, k := range keys { + packs = append(packs, orm.packs[uint(k)]) + } + + // Apply limit/offset + low, high := orm.getLimitOffsetSliceBounds(opt, len(packs)) + packs = packs[low:high] return packs, nil } diff --git a/server/datastore/inmem_queries.go b/server/datastore/inmem_queries.go index d654ae42ac..4934f6bcef 100644 --- a/server/datastore/inmem_queries.go +++ b/server/datastore/inmem_queries.go @@ -1,6 +1,8 @@ package datastore import ( + "sort" + "github.com/kolide/kolide-ose/server/kolide" ) @@ -58,14 +60,25 @@ func (orm *inmem) Query(id uint) (*kolide.Query, error) { return query, nil } -func (orm *inmem) Queries() ([]*kolide.Query, error) { +func (orm *inmem) Queries(opt kolide.ListOptions) ([]*kolide.Query, error) { orm.mtx.Lock() defer orm.mtx.Unlock() - queries := []*kolide.Query{} - for _, query := range orm.queries { - queries = append(queries, query) + // We need to sort by keys to provide reliable ordering + keys := []int{} + for k, _ := range orm.queries { + keys = append(keys, int(k)) } + sort.Ints(keys) + + queries := []*kolide.Query{} + for _, k := range keys { + queries = append(queries, orm.queries[uint(k)]) + } + + // Apply limit/offset + low, high := orm.getLimitOffsetSliceBounds(opt, len(queries)) + queries = queries[low:high] return queries, nil } diff --git a/server/datastore/inmem_test.go b/server/datastore/inmem_test.go new file mode 100644 index 0000000000..cf6b3a4f32 --- /dev/null +++ b/server/datastore/inmem_test.go @@ -0,0 +1,68 @@ +package datastore + +import ( + "testing" + + "github.com/kolide/kolide-ose/server/kolide" + "github.com/stretchr/testify/assert" +) + +func TestApplyLimitOffset(t *testing.T) { + im := inmem{} + data := []int{} + + // should work with empty + low, high := im.getLimitOffsetSliceBounds(kolide.ListOptions{}, len(data)) + result := data[low:high] + assert.Len(t, result, 0) + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: 1, PerPage: 20}, len(data)) + result = data[low:high] + assert.Len(t, result, 0) + + // insert some data + for i := 0; i < 100; i++ { + data = append(data, i) + } + + // unlimited + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{}, len(data)) + result = data[low:high] + assert.Len(t, result, 100) + assert.Equal(t, data, result) + + // reasonable limit page 0 + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{PerPage: 20}, len(data)) + result = data[low:high] + assert.Len(t, result, 20) + assert.Equal(t, data[:20], result) + + // too many per page + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{PerPage: 200}, len(data)) + result = data[low:high] + assert.Len(t, result, 100) + assert.Equal(t, data, result) + + // offset should be past end (zero results) + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: 1, PerPage: 200}, len(data)) + result = data[low:high] + assert.Len(t, result, 0) + + // all pages appended should equal the original data + result = []int{} + for i := 0; i < 5; i++ { // 5 used intentionally + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: uint(i), PerPage: 25}, len(data)) + result = append(result, data[low:high]...) + } + assert.Len(t, result, 100) + assert.Equal(t, data, result) + + // again with different params + result = []int{} + for i := 0; i < 100; i++ { // 5 used intentionally + low, high = im.getLimitOffsetSliceBounds(kolide.ListOptions{Page: uint(i), PerPage: 1}, len(data)) + result = append(result, data[low:high]...) + } + assert.Len(t, result, 100) + assert.Equal(t, data, result) + +} diff --git a/server/datastore/inmem_users.go b/server/datastore/inmem_users.go index cb9844e099..95ecf6fd74 100644 --- a/server/datastore/inmem_users.go +++ b/server/datastore/inmem_users.go @@ -1,6 +1,10 @@ package datastore -import "github.com/kolide/kolide-ose/server/kolide" +import ( + "sort" + + "github.com/kolide/kolide-ose/server/kolide" +) func (orm *inmem) NewUser(user *kolide.User) (*kolide.User, error) { orm.mtx.Lock() @@ -31,14 +35,25 @@ func (orm *inmem) User(username string) (*kolide.User, error) { return nil, ErrNotFound } -func (orm *inmem) Users() ([]*kolide.User, error) { +func (orm *inmem) Users(opt kolide.ListOptions) ([]*kolide.User, error) { orm.mtx.Lock() defer orm.mtx.Unlock() - var users []*kolide.User - for _, user := range orm.users { - users = append(users, user) + // We need to sort by keys to provide reliable ordering + keys := []int{} + for k, _ := range orm.users { + keys = append(keys, int(k)) } + sort.Ints(keys) + + users := []*kolide.User{} + for _, k := range keys { + users = append(users, orm.users[uint(k)]) + } + + // Apply limit/offset + low, high := orm.getLimitOffsetSliceBounds(opt, len(users)) + users = users[low:high] return users, nil } diff --git a/server/kolide/app.go b/server/kolide/app.go index 7f40839cd9..b7f320ba53 100644 --- a/server/kolide/app.go +++ b/server/kolide/app.go @@ -32,3 +32,13 @@ type OrgInfoPayload struct { OrgName *string `json:"org_name"` OrgLogoURL *string `json:"org_logo_url"` } + +// ListOptions defines options related to paging and ordering to be used when +// listing objects +type ListOptions struct { + // Which page to return (must be positive integer) + Page uint + // How many results per page (must be positive integer, 0 indicates + // unlimited) + PerPage uint +} diff --git a/server/kolide/hosts.go b/server/kolide/hosts.go index 9ef1101620..95127ac532 100644 --- a/server/kolide/hosts.go +++ b/server/kolide/hosts.go @@ -11,14 +11,14 @@ type HostStore interface { SaveHost(host *Host) error DeleteHost(host *Host) error Host(id uint) (*Host, error) - Hosts() ([]*Host, error) + Hosts(opt ListOptions) ([]*Host, error) EnrollHost(uuid, hostname, ip, platform string, nodeKeySize int) (*Host, error) AuthenticateHost(nodeKey string) (*Host, error) MarkHostSeen(host *Host, t time.Time) error } type HostService interface { - ListHosts(ctx context.Context) ([]*Host, error) + ListHosts(ctx context.Context, opt ListOptions) ([]*Host, error) GetHost(ctx context.Context, id uint) (*Host, error) HostStatus(ctx context.Context, host Host) string DeleteHost(ctx context.Context, id uint) error diff --git a/server/kolide/invite.go b/server/kolide/invite.go index e87775ad9c..e27b3a55c7 100644 --- a/server/kolide/invite.go +++ b/server/kolide/invite.go @@ -15,7 +15,7 @@ type InviteStore interface { NewInvite(i *Invite) (*Invite, error) // Invites lists all invites in the datastore. - Invites() ([]*Invite, error) + Invites(opt ListOptions) ([]*Invite, error) // Invite retrieves an invite by it's ID. Invite(id uint) (*Invite, error) @@ -40,7 +40,7 @@ type InviteService interface { DeleteInvite(ctx context.Context, id uint) (err error) // Invites returns a list of all invites. - Invites(ctx context.Context) (invites []*Invite, err error) + ListInvites(ctx context.Context, opt ListOptions) (invites []*Invite, err error) // VerifyInvite verifies that an invite exists and that it matches the // invite token. diff --git a/server/kolide/labels.go b/server/kolide/labels.go index 65bdbce024..830d87c1ae 100644 --- a/server/kolide/labels.go +++ b/server/kolide/labels.go @@ -12,7 +12,7 @@ type LabelStore interface { SaveLabel(Label *Label) error DeleteLabel(lid uint) error Label(lid uint) (*Label, error) - Labels() ([]*Label, error) + Labels(opt ListOptions) ([]*Label, error) // LabelQueriesForHost returns the label queries that should be executed // for the given host. The cutoff is the minimum timestamp a query @@ -32,7 +32,7 @@ type LabelStore interface { } type LabelService interface { - ListLabels(ctx context.Context) ([]*Label, error) + ListLabels(ctx context.Context, opt ListOptions) ([]*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) diff --git a/server/kolide/packs.go b/server/kolide/packs.go index 00f86bff8d..5d3a408f90 100644 --- a/server/kolide/packs.go +++ b/server/kolide/packs.go @@ -12,7 +12,7 @@ type PackStore interface { SavePack(pack *Pack) error DeletePack(pid uint) error Pack(pid uint) (*Pack, error) - Packs() ([]*Pack, error) + Packs(opt ListOptions) ([]*Pack, error) // Modifying the queries in packs AddQueryToPack(qid uint, pid uint) error @@ -29,7 +29,7 @@ type PackStore interface { } type PackService interface { - ListPacks(ctx context.Context) ([]*Pack, error) + ListPacks(ctx context.Context, opt ListOptions) ([]*Pack, error) GetPack(ctx context.Context, id uint) (*Pack, error) NewPack(ctx context.Context, p PackPayload) (*Pack, error) ModifyPack(ctx context.Context, id uint, p PackPayload) (*Pack, error) diff --git a/server/kolide/queries.go b/server/kolide/queries.go index 4e60b09cd6..54e80d00e3 100644 --- a/server/kolide/queries.go +++ b/server/kolide/queries.go @@ -12,11 +12,11 @@ type QueryStore interface { SaveQuery(query *Query) error DeleteQuery(query *Query) error Query(id uint) (*Query, error) - Queries() ([]*Query, error) + Queries(opt ListOptions) ([]*Query, error) } type QueryService interface { - ListQueries(ctx context.Context) ([]*Query, error) + ListQueries(ctx context.Context, opt ListOptions) ([]*Query, error) GetQuery(ctx context.Context, id uint) (*Query, error) NewQuery(ctx context.Context, p QueryPayload) (*Query, error) ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error) diff --git a/server/kolide/users.go b/server/kolide/users.go index 5dfd64db26..6afb2de0bc 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -14,7 +14,7 @@ import ( type UserStore interface { NewUser(user *User) (*User, error) User(username string) (*User, error) - Users() ([]*User, error) + Users(opt ListOptions) ([]*User, error) UserByEmail(email string) (*User, error) UserByID(id uint) (*User, error) SaveUser(user *User) error @@ -33,7 +33,7 @@ type UserService interface { AuthenticatedUser(ctx context.Context) (user *User, err error) // Users returns all users - Users(ctx context.Context) (users []*User, err error) + ListUsers(ctx context.Context, opt ListOptions) (users []*User, err error) // RequestPasswordReset generates a password reset request for // a user. The request results in a token emailed to the user. diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index dc6e904dcc..85c82d9657 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -41,6 +41,10 @@ func makeGetHostEndpoint(svc kolide.Service) endpoint.Endpoint { // List Hosts //////////////////////////////////////////////////////////////////////////////// +type listHostsRequest struct { + ListOptions kolide.ListOptions +} + type listHostsResponse struct { Hosts []hostResponse `json:"hosts"` Err error `json:"error,omitempty"` @@ -50,7 +54,8 @@ func (r listHostsResponse) error() error { return r.Err } func makeListHostsEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - hosts, err := svc.ListHosts(ctx) + req := request.(listHostsRequest) + hosts, err := svc.ListHosts(ctx, req.ListOptions) if err != nil { return listHostsResponse{Err: err}, nil } diff --git a/server/service/endpoint_invites.go b/server/service/endpoint_invites.go index d6adab653e..fb15dea4ff 100644 --- a/server/service/endpoint_invites.go +++ b/server/service/endpoint_invites.go @@ -28,6 +28,10 @@ func makeCreateInviteEndpoint(svc kolide.Service) endpoint.Endpoint { } } +type listInvitesRequest struct { + ListOptions kolide.ListOptions +} + type listInvitesResponse struct { Invites []kolide.Invite `json:"invites"` Err error `json:"error,omitempty"` @@ -37,7 +41,8 @@ func (r listInvitesResponse) error() error { return r.Err } func makeListInvitesEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - invites, err := svc.Invites(ctx) + req := request.(listInvitesRequest) + invites, err := svc.ListInvites(ctx, req.ListOptions) if err != nil { return listInvitesResponse{Err: err}, nil } diff --git a/server/service/endpoint_labels.go b/server/service/endpoint_labels.go index 103ce5f89e..9906ee20b0 100644 --- a/server/service/endpoint_labels.go +++ b/server/service/endpoint_labels.go @@ -36,6 +36,10 @@ func makeGetLabelEndpoint(svc kolide.Service) endpoint.Endpoint { // List Labels //////////////////////////////////////////////////////////////////////////////// +type listLabelsRequest struct { + ListOptions kolide.ListOptions +} + type listLabelsResponse struct { Labels []kolide.Label `json:"labels"` Err error `json:"error,omitempty"` @@ -45,7 +49,8 @@ func (r listLabelsResponse) error() error { return r.Err } func makeListLabelsEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - labels, err := svc.ListLabels(ctx) + req := request.(listLabelsRequest) + labels, err := svc.ListLabels(ctx, req.ListOptions) if err != nil { return listLabelsResponse{Err: err}, nil } diff --git a/server/service/endpoint_packs.go b/server/service/endpoint_packs.go index 27b5d677c7..819e937103 100644 --- a/server/service/endpoint_packs.go +++ b/server/service/endpoint_packs.go @@ -36,6 +36,10 @@ func makeGetPackEndpoint(svc kolide.Service) endpoint.Endpoint { // List Packs //////////////////////////////////////////////////////////////////////////////// +type listPacksRequest struct { + ListOptions kolide.ListOptions +} + type listPacksResponse struct { Packs []kolide.Pack `json:"packs"` Err error `json:"error,omitempty"` @@ -45,7 +49,8 @@ func (r listPacksResponse) error() error { return r.Err } func makeListPacksEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - packs, err := svc.ListPacks(ctx) + req := request.(listPacksRequest) + packs, err := svc.ListPacks(ctx, req.ListOptions) if err != nil { return getPackResponse{Err: err}, nil } diff --git a/server/service/endpoint_queries.go b/server/service/endpoint_queries.go index a323333b9e..f92278e4b5 100644 --- a/server/service/endpoint_queries.go +++ b/server/service/endpoint_queries.go @@ -35,6 +35,9 @@ func makeGetQueryEndpoint(svc kolide.Service) endpoint.Endpoint { //////////////////////////////////////////////////////////////////////////////// // List Queries //////////////////////////////////////////////////////////////////////////////// +type listQueriesRequest struct { + ListOptions kolide.ListOptions +} type listQueriesResponse struct { Queries []kolide.Query `json:"queries"` @@ -45,7 +48,8 @@ func (r listQueriesResponse) error() error { return r.Err } func makeListQueriesEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - queries, err := svc.ListQueries(ctx) + req := request.(listQueriesRequest) + queries, err := svc.ListQueries(ctx, req.ListOptions) if err != nil { return listQueriesResponse{Err: err}, nil } diff --git a/server/service/endpoint_users.go b/server/service/endpoint_users.go index 7d08df24ce..491dec5719 100644 --- a/server/service/endpoint_users.go +++ b/server/service/endpoint_users.go @@ -74,6 +74,10 @@ func makeGetSessionUserEndpoint(svc kolide.Service) endpoint.Endpoint { // List Users //////////////////////////////////////////////////////////////////////////////// +type listUsersRequest struct { + ListOptions kolide.ListOptions +} + type listUsersResponse struct { Users []kolide.User `json:"users"` Err error `json:"error,omitempty"` @@ -83,7 +87,8 @@ func (r listUsersResponse) error() error { return r.Err } func makeListUsersEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - users, err := svc.Users(ctx) + req := request.(listUsersRequest) + users, err := svc.ListUsers(ctx, req.ListOptions) if err != nil { return listUsersResponse{Err: err}, nil } diff --git a/server/service/handler.go b/server/service/handler.go index 5f86b4a552..18a5d3f9a1 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -181,7 +181,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt Me: newServer(e.Me, decodeNoParamsRequest), CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), GetUser: newServer(e.GetUser, decodeGetUserRequest), - ListUsers: newServer(e.ListUsers, decodeNoParamsRequest), + ListUsers: newServer(e.ListUsers, decodeListUsersRequest), ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), @@ -190,15 +190,15 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), - ListInvites: newServer(e.ListInvites, decodeNoParamsRequest), + ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), - ListQueries: newServer(e.ListQueries, decodeNoParamsRequest), + ListQueries: newServer(e.ListQueries, decodeListQueriesRequest), CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest), DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest), GetPack: newServer(e.GetPack, decodeGetPackRequest), - ListPacks: newServer(e.ListPacks, decodeNoParamsRequest), + ListPacks: newServer(e.ListPacks, decodeListPacksRequest), CreatePack: newServer(e.CreatePack, decodeCreatePackRequest), ModifyPack: newServer(e.ModifyPack, decodeModifyPackRequest), DeletePack: newServer(e.DeletePack, decodeDeletePackRequest), @@ -211,7 +211,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt SubmitDistributedQueryResults: newServer(e.SubmitDistributedQueryResults, decodeSubmitDistributedQueryResultsRequest), SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), GetLabel: newServer(e.GetLabel, decodeGetLabelRequest), - ListLabels: newServer(e.ListLabels, decodeNoParamsRequest), + ListLabels: newServer(e.ListLabels, decodeListLabelsRequest), CreateLabel: newServer(e.CreateLabel, decodeCreateLabelRequest), ModifyLabel: newServer(e.ModifyLabel, decodeModifyLabelRequest), DeleteLabel: newServer(e.DeleteLabel, decodeDeleteLabelRequest), @@ -220,7 +220,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt DeleteLabelFromPack: newServer(e.DeleteLabelFromPack, decodeDeleteLabelFromPackRequest), GetHost: newServer(e.GetHost, decodeGetHostRequest), DeleteHost: newServer(e.DeleteHost, decodeDeleteHostRequest), - ListHosts: newServer(e.ListHosts, decodeNoParamsRequest), + ListHosts: newServer(e.ListHosts, decodeListHostsRequest), } } diff --git a/server/service/logging_invites.go b/server/service/logging_invites.go index a355bdf187..271b483247 100644 --- a/server/service/logging_invites.go +++ b/server/service/logging_invites.go @@ -68,7 +68,7 @@ func (mw loggingMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, erro "took", time.Since(begin), ) }(time.Now()) - invites, err = mw.Service.Invites(ctx) + invites, err = mw.Service.ListInvites(ctx, kolide.ListOptions{}) return invites, err } diff --git a/server/service/metrics_invites.go b/server/service/metrics_invites.go index 2b6fef4ae4..1fce65270c 100644 --- a/server/service/metrics_invites.go +++ b/server/service/metrics_invites.go @@ -35,7 +35,7 @@ func (mw metricsMiddleware) DeleteInvite(ctx context.Context, id uint) error { return err } -func (mw metricsMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, error) { +func (mw metricsMiddleware) ListInvites(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Invite, error) { var ( invites []*kolide.Invite err error @@ -45,7 +45,7 @@ func (mw metricsMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, erro mw.requestCount.With(lvs...).Add(1) mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) }(time.Now()) - invites, err = mw.Service.Invites(ctx) + invites, err = mw.Service.ListInvites(ctx, opt) return invites, err } diff --git a/server/service/metrics_users.go b/server/service/metrics_users.go index f28da7fe5d..fb4f274801 100644 --- a/server/service/metrics_users.go +++ b/server/service/metrics_users.go @@ -55,7 +55,7 @@ func (mw metricsMiddleware) User(ctx context.Context, id uint) (*kolide.User, er return user, err } -func (mw metricsMiddleware) Users(ctx context.Context) ([]*kolide.User, error) { +func (mw metricsMiddleware) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) { var ( users []*kolide.User @@ -67,7 +67,7 @@ func (mw metricsMiddleware) Users(ctx context.Context) ([]*kolide.User, error) { mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) }(time.Now()) - users, err = mw.Service.Users(ctx) + users, err = mw.Service.ListUsers(ctx, opt) return users, err } diff --git a/server/service/service_hosts.go b/server/service/service_hosts.go index 77d7e386d5..f7aff001e3 100644 --- a/server/service/service_hosts.go +++ b/server/service/service_hosts.go @@ -7,8 +7,8 @@ import ( "golang.org/x/net/context" ) -func (svc service) ListHosts(ctx context.Context) ([]*kolide.Host, error) { - return svc.ds.Hosts() +func (svc service) ListHosts(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Host, error) { + return svc.ds.Hosts(opt) } func (svc service) GetHost(ctx context.Context, id uint) (*kolide.Host, error) { diff --git a/server/service/service_hosts_test.go b/server/service/service_hosts_test.go index 7665870fd0..d87c918387 100644 --- a/server/service/service_hosts_test.go +++ b/server/service/service_hosts_test.go @@ -18,7 +18,7 @@ func TestListHosts(t *testing.T) { ctx := context.Background() - hosts, err := svc.ListHosts(ctx) + hosts, err := svc.ListHosts(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) @@ -27,7 +27,7 @@ func TestListHosts(t *testing.T) { }) assert.Nil(t, err) - hosts, err = svc.ListHosts(ctx) + hosts, err = svc.ListHosts(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 1) } @@ -71,7 +71,7 @@ func TestDeleteHost(t *testing.T) { err = svc.DeleteHost(ctx, host.ID) assert.Nil(t, err) - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) diff --git a/server/service/service_invites.go b/server/service/service_invites.go index 932fef3577..cf7552639a 100644 --- a/server/service/service_invites.go +++ b/server/service/service_invites.go @@ -61,8 +61,8 @@ func (svc service) InviteNewUser(ctx context.Context, payload kolide.InvitePaylo return invite, nil } -func (svc service) Invites(ctx context.Context) ([]*kolide.Invite, error) { - return svc.ds.Invites() +func (svc service) ListInvites(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Invite, error) { + return svc.ds.Invites(opt) } func (svc service) VerifyInvite(ctx context.Context, email, token string) error { diff --git a/server/service/service_labels.go b/server/service/service_labels.go index 57f8ecd37f..6e590d2008 100644 --- a/server/service/service_labels.go +++ b/server/service/service_labels.go @@ -5,8 +5,8 @@ import ( "golang.org/x/net/context" ) -func (svc service) ListLabels(ctx context.Context) ([]*kolide.Label, error) { - return svc.ds.Labels() +func (svc service) ListLabels(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Label, error) { + return svc.ds.Labels(opt) } func (svc service) GetLabel(ctx context.Context, id uint) (*kolide.Label, error) { diff --git a/server/service/service_labels_test.go b/server/service/service_labels_test.go index 840deab2da..edf59b70ee 100644 --- a/server/service/service_labels_test.go +++ b/server/service/service_labels_test.go @@ -18,7 +18,7 @@ func TestListLabels(t *testing.T) { ctx := context.Background() - labels, err := svc.ListLabels(ctx) + labels, err := svc.ListLabels(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, labels, 0) @@ -28,7 +28,7 @@ func TestListLabels(t *testing.T) { }) assert.Nil(t, err) - labels, err = svc.ListLabels(ctx) + labels, err = svc.ListLabels(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, labels, 1) assert.Equal(t, "foo", labels[0].Name) @@ -75,7 +75,7 @@ func TestNewLabel(t *testing.T) { assert.Nil(t, err) - labels, err := ds.Labels() + labels, err := ds.Labels(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, labels, 1) assert.Equal(t, "foo", labels[0].Name) @@ -127,7 +127,7 @@ func TestDeleteLabel(t *testing.T) { err = svc.DeleteLabel(ctx, label.ID) assert.Nil(t, err) - labels, err := ds.Labels() + labels, err := ds.Labels(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, labels, 0) } diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index 673ef8aa53..532f87e2e5 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -26,7 +26,7 @@ func TestEnrollAgent(t *testing.T) { ctx := context.Background() - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) @@ -34,7 +34,7 @@ func TestEnrollAgent(t *testing.T) { assert.Nil(t, err) assert.NotEmpty(t, nodeKey) - hosts, err = ds.Hosts() + hosts, err = ds.Hosts(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 1) } @@ -48,7 +48,7 @@ func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) { ctx := context.Background() - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) @@ -56,7 +56,7 @@ func TestEnrollAgentIncorrectEnrollSecret(t *testing.T) { assert.NotNil(t, err) assert.Empty(t, nodeKey) - hosts, err = ds.Hosts() + hosts, err = ds.Hosts(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, hosts, 0) } @@ -75,7 +75,7 @@ func TestSubmitStatusLogs(t *testing.T) { _, err = svc.EnrollAgent(ctx, "", "host123") assert.Nil(t, err) - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hosts, 1) host := hosts[0] @@ -147,7 +147,7 @@ func TestSubmitResultLogs(t *testing.T) { _, err = svc.EnrollAgent(ctx, "", "host123") assert.Nil(t, err) - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hosts, 1) host := hosts[0] @@ -248,7 +248,7 @@ func TestLabelQueries(t *testing.T) { _, err = svc.EnrollAgent(ctx, "", "host123") assert.Nil(t, err) - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hosts, 1) host := hosts[0] @@ -410,14 +410,14 @@ func TestGetClientConfig(t *testing.T) { ctx := context.Background() - hosts, err := ds.Hosts() + hosts, err := ds.Hosts(kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hosts, 0) _, err = svc.EnrollAgent(ctx, "", "user.local") assert.Nil(t, err) - hosts, err = ds.Hosts() + hosts, err = ds.Hosts(kolide.ListOptions{}) require.Nil(t, err) require.Len(t, hosts, 1) host := hosts[0] diff --git a/server/service/service_packs.go b/server/service/service_packs.go index cce95b5951..2972a767ce 100644 --- a/server/service/service_packs.go +++ b/server/service/service_packs.go @@ -5,8 +5,8 @@ import ( "golang.org/x/net/context" ) -func (svc service) ListPacks(ctx context.Context) ([]*kolide.Pack, error) { - return svc.ds.Packs() +func (svc service) ListPacks(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Pack, error) { + return svc.ds.Packs(opt) } func (svc service) GetPack(ctx context.Context, id uint) (*kolide.Pack, error) { diff --git a/server/service/service_packs_test.go b/server/service/service_packs_test.go index 47307e4563..38cec29d0b 100644 --- a/server/service/service_packs_test.go +++ b/server/service/service_packs_test.go @@ -18,7 +18,7 @@ func TestListPacks(t *testing.T) { ctx := context.Background() - queries, err := svc.ListPacks(ctx) + queries, err := svc.ListPacks(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 0) @@ -27,7 +27,7 @@ func TestListPacks(t *testing.T) { }) assert.Nil(t, err) - queries, err = svc.ListPacks(ctx) + queries, err = svc.ListPacks(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 1) } @@ -70,7 +70,7 @@ func TestNewPack(t *testing.T) { assert.Nil(t, err) - queries, err := ds.Packs() + queries, err := ds.Packs(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 1) } @@ -120,7 +120,7 @@ func TestDeletePack(t *testing.T) { err = svc.DeletePack(ctx, pack.ID) assert.Nil(t, err) - queries, err := ds.Packs() + queries, err := ds.Packs(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 0) diff --git a/server/service/service_queries.go b/server/service/service_queries.go index cc7dc84fa7..13bf7bff65 100644 --- a/server/service/service_queries.go +++ b/server/service/service_queries.go @@ -5,8 +5,8 @@ import ( "golang.org/x/net/context" ) -func (svc service) ListQueries(ctx context.Context) ([]*kolide.Query, error) { - return svc.ds.Queries() +func (svc service) ListQueries(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Query, error) { + return svc.ds.Queries(opt) } func (svc service) GetQuery(ctx context.Context, id uint) (*kolide.Query, error) { diff --git a/server/service/service_queries_test.go b/server/service/service_queries_test.go index 02bec3b24d..1ab474bb1a 100644 --- a/server/service/service_queries_test.go +++ b/server/service/service_queries_test.go @@ -18,7 +18,7 @@ func TestListQueries(t *testing.T) { ctx := context.Background() - queries, err := svc.ListQueries(ctx) + queries, err := svc.ListQueries(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 0) @@ -28,7 +28,7 @@ func TestListQueries(t *testing.T) { }) assert.Nil(t, err) - queries, err = svc.ListQueries(ctx) + queries, err = svc.ListQueries(ctx, kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 1) } @@ -74,7 +74,7 @@ func TestNewQuery(t *testing.T) { assert.Nil(t, err) - queries, err := ds.Queries() + queries, err := ds.Queries(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 1) } @@ -126,7 +126,7 @@ func TestDeleteQuery(t *testing.T) { err = svc.DeleteQuery(ctx, query.ID) assert.Nil(t, err) - queries, err := ds.Queries() + queries, err := ds.Queries(kolide.ListOptions{}) assert.Nil(t, err) assert.Len(t, queries, 0) diff --git a/server/service/service_users.go b/server/service/service_users.go index 80b4b48364..c89ba3dd65 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -115,8 +115,8 @@ func (svc service) AuthenticatedUser(ctx context.Context) (*kolide.User, error) return vc.User, nil } -func (svc service) Users(ctx context.Context) ([]*kolide.User, error) { - return svc.ds.Users() +func (svc service) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) { + return svc.ds.Users(opt) } func (svc service) ResetPassword(ctx context.Context, token, password string) error { diff --git a/server/service/transport.go b/server/service/transport.go index 30e035082f..0e0f239101 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/gorilla/mux" + "github.com/kolide/kolide-ose/server/kolide" "golang.org/x/net/context" ) @@ -52,6 +53,50 @@ func idFromRequest(r *http.Request, name string) (uint, error) { return uint(uid), nil } +// default number of items to include per page +const defaultPerPage = 20 + +// listOptionsFromRequest parses the list options from the request parameters +func listOptionsFromRequest(r *http.Request) (kolide.ListOptions, error) { + var err error + + pageString := r.URL.Query().Get("page") + perPageString := r.URL.Query().Get("per_page") + + var page int = 0 + if pageString != "" { + page, err = strconv.Atoi(pageString) + if err != nil { + return kolide.ListOptions{}, errors.New("non-int page value") + } + if page < 0 { + return kolide.ListOptions{}, errors.New("negative page value") + } + } + + // We default to 0 for per_page so that not specifying any paging + // information gets all results + var perPage int = 0 + if perPageString != "" { + perPage, err = strconv.Atoi(perPageString) + if err != nil { + return kolide.ListOptions{}, errors.New("non-int per_page value") + } + if perPage <= 0 { + return kolide.ListOptions{}, errors.New("invalid per_page value") + } + } + + if perPage == 0 && pageString != "" { + // We explicitly set a non-zero default if a page is specified + // (because the client probably intended for paging, and + // leaving the 0 would turn that off) + perPage = defaultPerPage + } + + return kolide.ListOptions{Page: uint(page), PerPage: uint(perPage)}, nil +} + func decodeNoParamsRequest(ctx context.Context, r *http.Request) (interface{}, error) { return nil, nil } diff --git a/server/service/transport_hosts.go b/server/service/transport_hosts.go index bbda13a75f..2855b33ee3 100644 --- a/server/service/transport_hosts.go +++ b/server/service/transport_hosts.go @@ -21,3 +21,11 @@ func decodeDeleteHostRequest(ctx context.Context, r *http.Request) (interface{}, } return deleteHostRequest{ID: id}, nil } + +func decodeListHostsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listHostsRequest{ListOptions: opt}, nil +} diff --git a/server/service/transport_invites.go b/server/service/transport_invites.go index 4f0478f627..c708c5ffa5 100644 --- a/server/service/transport_invites.go +++ b/server/service/transport_invites.go @@ -32,3 +32,11 @@ func decodeDeleteInviteRequest(ctx context.Context, r *http.Request) (interface{ req.ID = id return req, nil } + +func decodeListInvitesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listInvitesRequest{ListOptions: opt}, nil +} diff --git a/server/service/transport_labels.go b/server/service/transport_labels.go index 9a1df7321b..91f7495808 100644 --- a/server/service/transport_labels.go +++ b/server/service/transport_labels.go @@ -47,3 +47,11 @@ func decodeGetLabelRequest(ctx context.Context, r *http.Request) (interface{}, e req.ID = id return req, nil } + +func decodeListLabelsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listLabelsRequest{ListOptions: opt}, nil +} diff --git a/server/service/transport_packs.go b/server/service/transport_packs.go index 494806f166..6dd2545315 100644 --- a/server/service/transport_packs.go +++ b/server/service/transport_packs.go @@ -49,6 +49,14 @@ func decodeGetPackRequest(ctx context.Context, r *http.Request) (interface{}, er return req, nil } +func decodeListPacksRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listPacksRequest{ListOptions: opt}, nil +} + func decodeAddQueryToPackRequest(ctx context.Context, r *http.Request) (interface{}, error) { qid, err := idFromRequest(r, "qid") if err != nil { diff --git a/server/service/transport_queries.go b/server/service/transport_queries.go index f1e0ed1e4d..f5c1c3ef38 100644 --- a/server/service/transport_queries.go +++ b/server/service/transport_queries.go @@ -47,3 +47,11 @@ func decodeGetQueryRequest(ctx context.Context, r *http.Request) (interface{}, e req.ID = id return req, nil } + +func decodeListQueriesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listQueriesRequest{ListOptions: opt}, nil +} diff --git a/server/service/transport_test.go b/server/service/transport_test.go new file mode 100644 index 0000000000..4c2ca5b0fc --- /dev/null +++ b/server/service/transport_test.go @@ -0,0 +1,78 @@ +package service + +import ( + "net/http" + "net/url" + "testing" + + "github.com/kolide/kolide-ose/server/kolide" + "github.com/stretchr/testify/assert" +) + +func TestListOptionsFromRequest(t *testing.T) { + var listOptionsTests = []struct { + // url string to parse + url string + // expected list options + listOptions kolide.ListOptions + // should cause an error + shouldErr bool + }{ + // both params provided + { + url: "/foo?page=1&per_page=10", + listOptions: kolide.ListOptions{Page: 1, PerPage: 10}, + }, + // only per_page (page should default to 0) + { + url: "/foo?per_page=10", + listOptions: kolide.ListOptions{Page: 0, PerPage: 10}, + }, + // only page (per_page should default to defaultPerPage + { + url: "/foo?page=10", + listOptions: kolide.ListOptions{Page: 10, PerPage: defaultPerPage}, + }, + // no params provided (defaults to empty ListOptions indicating + // unlimited) + { + url: "/foo?unrelated=foo", + listOptions: kolide.ListOptions{}, + }, + + // various error cases + { + url: "/foo?page=foo&per_page=10", + shouldErr: true, + }, + { + url: "/foo?page=1&per_page=foo", + shouldErr: true, + }, + { + url: "/foo?page=-1", + shouldErr: true, + }, + { + url: "/foo?page=-1&per_page=-10", + shouldErr: true, + }, + } + + for _, tt := range listOptionsTests { + t.Run(tt.url, func(t *testing.T) { + url, _ := url.Parse(tt.url) + req := &http.Request{URL: url} + opt, err := listOptionsFromRequest(req) + + if tt.shouldErr { + assert.NotNil(t, err) + return + } + + assert.Nil(t, err) + assert.Equal(t, tt.listOptions, opt) + + }) + } +} diff --git a/server/service/transport_users.go b/server/service/transport_users.go index 82033410a2..f41121680f 100644 --- a/server/service/transport_users.go +++ b/server/service/transport_users.go @@ -24,6 +24,14 @@ func decodeGetUserRequest(ctx context.Context, r *http.Request) (interface{}, er return getUserRequest{ID: id}, nil } +func decodeListUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listUsersRequest{ListOptions: opt}, nil +} + func decodeChangePasswordRequest(ctx context.Context, r *http.Request) (interface{}, error) { var req resetPasswordRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil {