Merge branch 'main' into 14415

This commit is contained in:
Jacob Shandling 2023-12-12 10:37:41 -08:00
commit 3a482f56b7
15 changed files with 110 additions and 41 deletions

View file

@ -0,0 +1 @@
- Fixes bug where Global Observers were not able to list all queries through the API.

View file

@ -774,9 +774,9 @@ func TestGetSoftwareVersions(t *testing.T) {
var gotTeamID *uint
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
gotTeamID = opt.TeamID
return []fleet.Software{foo001, foo002, foo003, bar003}, nil
return []fleet.Software{foo001, foo002, foo003, bar003}, &fleet.PaginationMetadata{}, nil
}
ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {

View file

@ -6,7 +6,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
)
func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOptions) ([]fleet.Software, error) {
func (svc *Service) ListSoftware(ctx context.Context, opts fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
// reuse ListSoftware, but include cve scores in premium version
opts.IncludeCVEScores = true
return svc.Service.ListSoftware(ctx, opts)

View file

@ -90,6 +90,7 @@ const ManageQueriesPage = ({
filteredQueriesPath,
isPremiumTier,
isSandboxMode,
isGlobalObserver,
config,
} = useContext(AppContext);
const { setLastEditedQueryBody, setSelectedQueryTargetsByType } = useContext(
@ -137,6 +138,12 @@ const ManageQueriesPage = ({
[{ scope: "queries", teamId: teamIdForApi }],
({ queryKey: [{ teamId }] }) =>
queriesAPI.loadAll(teamId).then(({ queries }) => {
if (isGlobalObserver) {
return queries
.filter((q: ISchedulableQuery) => q.observer_can_run)
.map(enhanceQuery);
}
return queries.map(enhanceQuery);
}),
{

View file

@ -787,12 +787,18 @@ func appendLimitOffsetToSelect(ds *goqu.SelectDataset, opts fleet.ListOptions) *
if perPage == 0 {
perPage = defaultSelectLimit
}
ds = ds.Limit(perPage)
offset := perPage * opts.Page
if offset > 0 {
ds = ds.Offset(offset)
}
if opts.IncludeMetadata {
perPage++
}
ds = ds.Limit(perPage)
return ds
}

View file

@ -1036,8 +1036,26 @@ func (ds *Datastore) ListSoftwareCPEs(ctx context.Context) ([]fleet.SoftwareCPE,
return result, nil
}
func (ds *Datastore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
return listSoftwareDB(ctx, ds.reader(ctx), opt)
func (ds *Datastore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
software, err := listSoftwareDB(ctx, ds.reader(ctx), opt)
if err != nil {
return nil, nil, err
}
perPage := opt.ListOptions.PerPage
var metaData *fleet.PaginationMetadata
if opt.ListOptions.IncludeMetadata {
if perPage <= 0 {
perPage = defaultSelectLimit
}
metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0}
if len(software) > int(perPage) {
metaData.HasNextResults = true
software = software[:len(software)-1]
}
}
return software, metaData, nil
}
func (ds *Datastore) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {

View file

@ -657,9 +657,10 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
t.Run("paginates", func(t *testing.T) {
opts := fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{
Page: 1,
PerPage: 1,
OrderKey: "version",
Page: 1,
PerPage: 1,
OrderKey: "version",
IncludeMetadata: true,
},
IncludeCVEScores: true,
}
@ -704,9 +705,10 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
opts := fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{
PerPage: 1,
Page: 1,
OrderKey: "id",
PerPage: 1,
Page: 1,
OrderKey: "id",
IncludeMetadata: true,
},
TeamID: &team1.ID,
}
@ -882,12 +884,30 @@ func testSoftwareList(t *testing.T, ds *Datastore) {
}
func listSoftwareCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareListOptions, returnSorted bool) []fleet.Software {
software, err := ds.ListSoftware(context.Background(), opts)
software, meta, err := ds.ListSoftware(context.Background(), opts)
require.NoError(t, err)
require.Len(t, software, expectedListCount)
count, err := ds.CountSoftware(context.Background(), opts)
require.NoError(t, err)
require.Equal(t, expectedFullCount, count)
if opts.ListOptions.IncludeMetadata {
require.NotNil(t, meta)
if expectedListCount == expectedFullCount {
require.False(t, meta.HasPreviousResults)
require.True(t, meta.HasNextResults)
}
if expectedFullCount > expectedListCount {
shouldHavePrevious := opts.ListOptions.Page > 0
require.Equal(t, shouldHavePrevious, meta.HasPreviousResults)
shouldHaveNext := uint(expectedFullCount) > (opts.ListOptions.Page+1)*opts.ListOptions.PerPage // page is 0-indexed
require.Equal(t, shouldHaveNext, meta.HasNextResults)
}
} else {
require.Nil(t, meta)
}
for _, s := range software {
sort.Slice(s.Vulnerabilities, func(i, j int) bool {
return s.Vulnerabilities[i].CVE < s.Vulnerabilities[j].CVE
@ -1372,7 +1392,7 @@ func testHostVulnSummariesBySoftwareIDs(t *testing.T, ds *Datastore) {
insertVulnSoftwareForTest(t, ds)
allSoftware, err := ds.ListSoftware(ctx, fleet.SoftwareListOptions{})
allSoftware, _, err := ds.ListSoftware(ctx, fleet.SoftwareListOptions{})
require.NoError(t, err)
var fooRpm fleet.Software

View file

@ -559,7 +559,7 @@ type Datastore interface {
// MigrationStatus returns nil if migrations are complete, and an error if migrations need to be run.
MigrationStatus(ctx context.Context) (*MigrationStatus, error)
ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, error)
ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, *PaginationMetadata, error)
CountSoftware(ctx context.Context, opt SoftwareListOptions) (int, error)
// DeleteVulnerabilities deletes the given list of vulnerabilities identified by CPE+CVE.
DeleteSoftwareVulnerabilities(ctx context.Context, vulnerabilities []SoftwareVulnerability) error

View file

@ -570,7 +570,7 @@ type Service interface {
// /////////////////////////////////////////////////////////////////////////////
// Software
ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, error)
ListSoftware(ctx context.Context, opt SoftwareListOptions) ([]Software, *PaginationMetadata, error)
SoftwareByID(ctx context.Context, id uint, includeCVEScores bool) (*Software, error)
CountSoftware(ctx context.Context, opt SoftwareListOptions) (int, error)

View file

@ -416,7 +416,7 @@ type MigrateDataFunc func(ctx context.Context) error
type MigrationStatusFunc func(ctx context.Context) (*fleet.MigrationStatus, error)
type ListSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error)
type ListSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error)
type CountSoftwareFunc func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error)
@ -3305,7 +3305,7 @@ func (s *DataStore) MigrationStatus(ctx context.Context) (*fleet.MigrationStatus
return s.MigrationStatusFunc(ctx)
}
func (s *DataStore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
func (s *DataStore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
s.mu.Lock()
s.ListSoftwareFuncInvoked = true
s.mu.Unlock()

View file

@ -5757,6 +5757,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
versResp = listSoftwareVersionsResponse{}
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "0", "order_key", "hosts_count", "order_direction", "desc")
assertVersionsResp(versResp, []fleet.Software{sws[19], sws[18], sws[17], sws[16], sws[15]}, hostsCountTs, len(sws), 20, 19, 18, 17, 16)
require.False(t, versResp.Meta.HasPreviousResults)
require.True(t, versResp.Meta.HasNextResults)
// second page (page=1)
lsResp = listSoftwareResponse{}
@ -5765,6 +5767,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
versResp = listSoftwareVersionsResponse{}
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "1", "order_key", "hosts_count", "order_direction", "desc")
assertVersionsResp(versResp, []fleet.Software{sws[14], sws[13], sws[12], sws[11], sws[10]}, hostsCountTs, len(sws), 15, 14, 13, 12, 11)
require.True(t, versResp.Meta.HasPreviousResults)
require.True(t, versResp.Meta.HasNextResults)
// third page (page=2)
lsResp = listSoftwareResponse{}
@ -5773,6 +5777,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
versResp = listSoftwareVersionsResponse{}
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "2", "order_key", "hosts_count", "order_direction", "desc")
assertVersionsResp(versResp, []fleet.Software{sws[9], sws[8], sws[7], sws[6], sws[5]}, hostsCountTs, len(sws), 10, 9, 8, 7, 6)
require.True(t, versResp.Meta.HasPreviousResults)
require.True(t, versResp.Meta.HasNextResults)
// last page (page=3)
lsResp = listSoftwareResponse{}
@ -5781,6 +5787,8 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
versResp = listSoftwareVersionsResponse{}
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versResp, "per_page", "5", "page", "3", "order_key", "hosts_count", "order_direction", "desc")
assertVersionsResp(versResp, []fleet.Software{sws[4], sws[3], sws[2], sws[1], sws[0]}, hostsCountTs, len(sws), 5, 4, 3, 2, 1)
require.True(t, versResp.Meta.HasPreviousResults)
require.False(t, versResp.Meta.HasNextResults)
// past the end
lsResp = listSoftwareResponse{}

View file

@ -114,7 +114,10 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team
func onlyShowObserverCanRunQueries(user *fleet.User, teamID *uint) bool {
if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleObserver {
return true
// Return false here because Global Observers should be able to access all queries via API.
// However, the UI will only show queries that have "observer can run" set to true.
// See the user permissions matrix: https://fleetdm.com/docs/using-fleet/manage-access#user-permissions
return false
}
return teamID != nil && user.TeamMembership(func(ut fleet.UserTeam) bool {

View file

@ -14,7 +14,7 @@ import (
func TestFilterQueriesForObserver(t *testing.T) {
t.Run("global role", func(t *testing.T) {
require.True(t, onlyShowObserverCanRunQueries(&fleet.User{
require.False(t, onlyShowObserverCanRunQueries(&fleet.User{
GlobalRole: ptr.String(fleet.RoleObserver),
}, nil))
@ -89,7 +89,7 @@ func TestListQueries(t *testing.T) {
{
title: "global observer",
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
expectedOpts: fleet.ListQueryOptions{OnlyObserverCanRun: true},
expectedOpts: fleet.ListQueryOptions{OnlyObserverCanRun: false},
},
{
title: "team maintainer",

View file

@ -29,7 +29,7 @@ func (r listSoftwareResponse) error() error { return r.Err }
// DEPRECATED: use listSoftwareVersionsEndpoint instead
func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*listSoftwareRequest)
resp, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
resp, _, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return listSoftwareResponse{Err: err}, nil
}
@ -50,17 +50,23 @@ func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Se
}
type listSoftwareVersionsResponse struct {
Count int `json:"count"`
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
Software []fleet.Software `json:"software,omitempty"`
Err error `json:"error,omitempty"`
Count int `json:"count"`
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
Software []fleet.Software `json:"software,omitempty"`
Meta *fleet.PaginationMetadata `json:"meta"`
Err error `json:"error,omitempty"`
}
func (r listSoftwareVersionsResponse) error() error { return r.Err }
func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*listSoftwareRequest)
resp, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
// always include pagination for new software versions endpoint (not included by default in
// legacy endpoint for backwards compatibility)
req.SoftwareListOptions.ListOptions.IncludeMetadata = true
resp, meta, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
if err != nil {
return listSoftwareVersionsResponse{Err: err}, nil
}
@ -72,7 +78,7 @@ func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc
latest = sw.CountsUpdatedAt
}
}
listResp := listSoftwareVersionsResponse{Software: resp}
listResp := listSoftwareVersionsResponse{Software: resp, Meta: meta}
if !latest.IsZero() {
listResp.CountsUpdatedAt = &latest
}
@ -86,11 +92,11 @@ func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc
return listResp, nil
}
func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
TeamID: opt.TeamID,
}, fleet.ActionRead); err != nil {
return nil, err
return nil, nil, err
}
// default sort order to hosts_count descending
@ -100,12 +106,12 @@ func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOpti
}
opt.WithHostCounts = true
softwares, err := svc.ds.ListSoftware(ctx, opt)
softwares, meta, err := svc.ds.ListSoftware(ctx, opt)
if err != nil {
return nil, err
return nil, nil, err
}
return softwares, nil
return softwares, meta, nil
}
/////////////////////////////////////////////////////////////////////////////////

View file

@ -17,10 +17,10 @@ func TestService_ListSoftware(t *testing.T) {
var calledWithTeamID *uint
var calledWithOpt fleet.SoftwareListOptions
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
calledWithTeamID = opt.TeamID
calledWithOpt = opt
return []fleet.Software{}, nil
return []fleet.Software{}, &fleet.PaginationMetadata{}, nil
}
user := &fleet.User{
@ -32,7 +32,7 @@ func TestService_ListSoftware(t *testing.T) {
svc, ctx := newTestService(t, ds, nil, nil)
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
_, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: ptr.Uint(42), ListOptions: fleet.ListOptions{PerPage: 77, Page: 4}})
_, _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: ptr.Uint(42), ListOptions: fleet.ListOptions{PerPage: 77, Page: 4}})
require.NoError(t, err)
assert.True(t, ds.ListSoftwareFuncInvoked)
@ -43,7 +43,7 @@ func TestService_ListSoftware(t *testing.T) {
// call again, this time with an explicit sort
ds.ListSoftwareFuncInvoked = false
_, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: nil, ListOptions: fleet.ListOptions{PerPage: 11, Page: 2, OrderKey: "id", OrderDirection: fleet.OrderAscending}})
_, _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{TeamID: nil, ListOptions: fleet.ListOptions{PerPage: 11, Page: 2, OrderKey: "id", OrderDirection: fleet.OrderAscending}})
require.NoError(t, err)
assert.True(t, ds.ListSoftwareFuncInvoked)
@ -55,8 +55,8 @@ func TestService_ListSoftware(t *testing.T) {
func TestServiceSoftwareInventoryAuth(t *testing.T) {
ds := new(mock.Store)
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, error) {
return []fleet.Software{}, nil
ds.ListSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
return []fleet.Software{}, &fleet.PaginationMetadata{}, nil
}
ds.CountSoftwareFunc = func(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
return 0, nil
@ -173,7 +173,7 @@ func TestServiceSoftwareInventoryAuth(t *testing.T) {
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tc.user})
// List all software.
_, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{})
_, _, err := svc.ListSoftware(ctx, fleet.SoftwareListOptions{})
checkAuthErr(t, tc.shouldFailGlobalRead, err)
// Count all software.
@ -181,7 +181,7 @@ func TestServiceSoftwareInventoryAuth(t *testing.T) {
checkAuthErr(t, tc.shouldFailGlobalRead, err)
// List software for a team.
_, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{
_, _, err = svc.ListSoftware(ctx, fleet.SoftwareListOptions{
TeamID: ptr.Uint(1),
})
checkAuthErr(t, tc.shouldFailTeamRead, err)