diff --git a/changes/issue-10409-no-team-filter b/changes/issue-10409-no-team-filter new file mode 100644 index 0000000000..5dcf691f54 --- /dev/null +++ b/changes/issue-10409-no-team-filter @@ -0,0 +1,2 @@ +- Updated API endpoints that use `team_id` query parameter so that `team_id=0` + filters results to include only hosts that are not assigned to any team. diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index b6f6482d8c..be6ed13e9f 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -1285,6 +1285,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts)) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts)) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), []*fleet.Host{})) // hosts[0] and hosts[1] failed one profile upsertHostCPs(hosts[0:2], noTeamCPs[0:1], fleet.MDMAppleOperationTypeInstall, fleet.MDMAppleDeliveryFailed) @@ -1299,6 +1302,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts[2:])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts[2:])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), []*fleet.Host{})) // hosts[0:3] applied a third profile upsertHostCPs(hosts[0:3], noTeamCPs[2:3], fleet.MDMAppleOperationTypeInstall, fleet.MDMAppleDeliveryApplied) @@ -1311,6 +1317,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts[2:])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts[2:])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), []*fleet.Host{})) // hosts[9] applied all profiles upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeInstall, fleet.MDMAppleDeliveryApplied) @@ -1323,6 +1332,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts[2:9])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, hosts[9:10])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts[2:9])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), hosts[9:10])) // create a team tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "rocket"}) @@ -1352,6 +1364,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts[2:9])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts[2:9])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), []*fleet.Host{})) res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &tm.ID) // get summary for new team require.NoError(t, err) @@ -1405,6 +1420,9 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, nil, hosts[2:9])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusPending, ptr.Uint(0), hosts[2:9])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusFailing, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.MacOSSettingsStatusLatest, ptr.Uint(0), []*fleet.Host{})) } func testMDMAppleInsertIdPAccount(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 5ae5a6e593..60111241c1 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -762,10 +762,20 @@ func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, fil } func filterHostsByTeam(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { - if opt.TeamFilter != nil { - sql += ` AND h.team_id = ?` - params = append(params, *opt.TeamFilter) + if opt.TeamFilter == nil { + // default "all teams" option + return sql, params } + + if *opt.TeamFilter == uint(0) { + // "no team" option (where TeamFilter is explicitly zero) excludes hosts that are assigned to any team + sql += ` AND h.team_id IS NULL` + return sql, params + } + + sql += ` AND h.team_id = ?` + params = append(params, *opt.TeamFilter) + return sql, params } @@ -840,8 +850,9 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par newSQL := "" newParams := []interface{}{} - if opt.TeamFilter == nil || *opt.TeamFilter == 0 { - // add "no team" filter + if opt.TeamFilter == nil { + // macOS settings filter is not compatible with the "all teams" option so append the "no + // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) newSQL += ` AND h.team_id IS NULL` } diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index f7f74a6d02..267d3110b1 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -76,6 +76,7 @@ func TestHosts(t *testing.T) { {"SavePackStatsOverwrites", testHostsSavePackStatsOverwrites}, {"WithTeamPackStats", testHostsWithTeamPackStats}, {"Delete", testHostsDelete}, + {"HostListOptionsTeamFilter", testHostListOptionsTeamFilter}, {"ListFilterAdditional", testHostsListFilterAdditional}, {"ListStatus", testHostsListStatus}, {"ListQuery", testHostsListQuery}, @@ -660,6 +661,82 @@ func listHostsCheckCount(t *testing.T, ds *Datastore, filter fleet.TeamFilter, o return hosts } +func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { + var teamIDFilterNil *uint // "All teams" option should include all hosts regardless of team assignment + var teamIDFilterZero *uint = ptr.Uint(0) // "No team" option should include only hosts that are not assigned to any team + + team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"}) + require.NoError(t, err) + + var hosts []*fleet.Host + for i := 0; i < 10; i++ { + h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now()) + hosts = append(hosts, h) + } + userFilter := fleet.TeamFilter{User: test.UserAdmin} + + // confirm intial state + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID}, 0) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) + + // assign three hosts to team 1 + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID})) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-3) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID}, 3) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) + + // assign four hosts to team 2 + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hosts[3].ID, hosts[4].ID, hosts[5].ID, hosts[6].ID})) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-7) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID}, 3) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 4) + + // test team filter in combination with macos settings filter + require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileID: 1, + ProfileIdentifier: "identifier", + HostUUID: hosts[0].UUID, // hosts[0] is assgined to team 1 + CommandUUID: "command-uuid-1", + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryApplied, + }, + })) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: "latest"}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: "latest"}, 0) // wrong team + // macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: "latest"}, 0) // no team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: "latest"}, 0) // no team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: "latest"}, 0) // no team + + require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileID: 1, + ProfileIdentifier: "identifier", + HostUUID: hosts[9].UUID, // hosts[9] is assgined to no team + CommandUUID: "command-uuid-2", + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryApplied, + }, + })) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: "latest"}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: "latest"}, 0) // wrong team + // macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: "latest"}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: "latest"}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: "latest"}, 1) // hosts[9] +} + func testHostsListFilterAdditional(t *testing.T, ds *Datastore) { h, err := ds.NewHost(context.Background(), &fleet.Host{ DetailUpdatedAt: time.Now(), @@ -776,6 +853,9 @@ func testHostsListQuery(t *testing.T, ds *Datastore) { filter := fleet.TeamFilter{User: test.UserAdmin} + var teamIDFilterNil *uint // "All teams" filter + var teamIDFilterZero *uint = ptr.Uint(0) // "No team" filter + team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"}) require.NoError(t, err) team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"}) @@ -794,9 +874,12 @@ func testHostsListQuery(t *testing.T, ds *Datastore) { gotHosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) assert.Equal(t, 0, len(gotHosts)) - gotHosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{TeamFilter: nil}, len(hosts)) + gotHosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts)) assert.Equal(t, len(hosts), len(gotHosts)) + gotHosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, 0) + assert.Equal(t, 0, len(gotHosts)) + gotHosts = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{LowDiskSpaceFilter: ptr.Int(32)}, 3) assert.Equal(t, 3, len(gotHosts)) diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 7279ea6de9..8d72db97b3 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -558,6 +558,7 @@ func (ds *Datastore) applyHostLabelFilters(filter fleet.TeamFilter, lid uint, qu query, params = filterHostsByStatus(ds.clock.Now(), query, opt, params) query, params = filterHostsByTeam(query, opt, params) query, params = filterHostsByMDM(query, opt, params) + query, params = filterHostsByMacOSSettingsStatus(query, opt, params) query, params = searchLike(query, params, opt.MatchQuery, hostSearchColumns...) query = appendListOptionsToSQL(query, &opt.ListOptions) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index b6ee54c495..f24580f0a0 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -445,28 +445,68 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da require.NoError(t, db.AddHostsToTeam(context.Background(), &team1.ID, []uint{h1.ID})) - filter := fleet.TeamFilter{User: test.UserAdmin} + userFilter := fleet.TeamFilter{User: test.UserAdmin} + var teamIDFilterNil *uint // "All teams" option should include all hosts regardless of team assignment + var teamIDFilterZero *uint = ptr.Uint(0) // "No team" option should include only hosts that are not assigned to any team + for _, h := range []*fleet.Host{h1, h2} { err = db.RecordLabelQueryExecutions(context.Background(), h, map[uint]*bool{l1.ID: ptr.Bool(true)}, time.Now(), deferred) assert.Nil(t, err) } { - hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusOnline}, 1) + hosts := listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusOnline}, 1) assert.Equal(t, "foo.local", hosts[0].Hostname) } { - hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusMIA}, 1) + hosts := listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{StatusFilter: fleet.StatusMIA}, 1) assert.Equal(t, "bar.local", hosts[0].Hostname) } { - hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID}, 1) + hosts := listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID}, 1) assert.Equal(t, "foo.local", hosts[0].Hostname) } - listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID}, 0) // no hosts assigned to team 2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, 1) // h2 not assigned to any team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, 2) // h1 and h2 + + // test team filter in combination with macos settings filter + require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileID: 1, + ProfileIdentifier: "identifier", + HostUUID: h1.UUID, // hosts[0] is assgined to team 1 + CommandUUID: "command-uuid-1", + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryApplied, + }, + })) + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: "latest"}, 1) // h1 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: "latest"}, 0) // wrong team + // macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: "latest"}, 0) // no team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: "latest"}, 0) // no team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: "latest"}, 0) // no team + + require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileID: 1, + ProfileIdentifier: "identifier", + HostUUID: h2.UUID, // hosts[9] is assgined to no team + CommandUUID: "command-uuid-2", + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryApplied, + }, + })) + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: "latest"}, 1) // h1 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: "latest"}, 0) // wrong team + // macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: "latest"}, 1) // h2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: "latest"}, 1) // h2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: "latest"}, 1) // h2 } func testLabelsBuiltIn(t *testing.T, db *Datastore) { diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index ea9041230c..eccce79efb 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -75,6 +75,8 @@ func (s MacOSSettingsStatus) IsValid() bool { // - GET /hosts/count (count hosts, which calls svc.CountHosts or svc.CountHostsInLabel) // - GET /labels/{id}/hosts (list hosts in label) // - GET /hosts/report +// - POST /hosts/delete (calls svc.hostIDsFromFilters) +// - POST /hosts/transfer/filter (calls svc.hostIDsFromFilters) // // Make sure the docs are updated accordingly and all endpoints behave as expected. type HostListOptions struct {