Fix fleet-scoped host vitals labels (#46953)
**Related issue:** Resolves #46869 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Host vitals labels based on identity-provider group membership now correctly apply to both global and team-scoped hosts, preventing cross-team leakage. * **Tests** * Added and updated tests to validate IdP-group-backed vitals label membership across global and per-team hosts. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+6
-1
@@ -2068,7 +2068,12 @@ func cronHostVitalsLabelMembership(
|
||||
) error {
|
||||
// Get all labels. We don't have a function for labels by membership type
|
||||
// so we'll filter them later.
|
||||
labels, err := ds.ListLabels(ctx, fleet.TeamFilter{}, fleet.ListOptions{
|
||||
//
|
||||
// We use a global admin filter so that fleet/team-scoped labels are included.
|
||||
// An empty TeamFilter (nil User) falls back to the "global-only" filter
|
||||
// (l.team_id IS NULL), which would silently exclude every fleet-scoped host
|
||||
// vitals label and leave them unpopulated. See #46869.
|
||||
labels, err := ds.ListLabels(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: new(fleet.RoleAdmin)}}, fleet.ListOptions{
|
||||
PerPage: 0, // No limit.
|
||||
}, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql/mysqltest"
|
||||
@@ -345,3 +346,94 @@ func TestBuildChartScopeResolver(t *testing.T) {
|
||||
require.Nil(t, disabled)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostVitalsLabelMembershipCronIDP(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ds := mysqltest.CreateMySQLDS(t)
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "idp-cron-team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "idp-cron-team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// host0 -> team1, host1 -> team2, host2 -> global (no team).
|
||||
hosts := make([]*fleet.Host, 3)
|
||||
teamIDs := []*uint{&team1.ID, &team2.ID, nil}
|
||||
for i := range 3 {
|
||||
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||
OsqueryHostID: new(fmt.Sprintf("idp-cron-%d", i)),
|
||||
NodeKey: new(fmt.Sprintf("idp-cron-%d", i)),
|
||||
UUID: fmt.Sprintf("idp-cron-uuid%d", i),
|
||||
Hostname: fmt.Sprintf("idp-cron-host%d.local", i),
|
||||
HardwareSerial: fmt.Sprintf("idp-cron-hwd%d", i),
|
||||
Platform: "darwin",
|
||||
TeamID: teamIDs[i],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hosts[i] = h
|
||||
}
|
||||
|
||||
// All three SCIM users are in the same "Engineering" IdP group.
|
||||
scimUserIDs := make([]uint, 3)
|
||||
for i := range 3 {
|
||||
id, err := ds.CreateScimUser(ctx, &fleet.ScimUser{
|
||||
UserName: fmt.Sprintf("idp-cron-user%d", i),
|
||||
Active: new(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
scimUserIDs[i] = id
|
||||
hostID, scimUserID := hosts[i].ID, id
|
||||
mysqltest.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
"INSERT INTO host_scim_user (host_id, scim_user_id) VALUES (?, ?)",
|
||||
hostID, scimUserID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
_, err = ds.CreateScimGroup(ctx, &fleet.ScimGroup{DisplayName: "Engineering", ScimUsers: scimUserIDs})
|
||||
require.NoError(t, err)
|
||||
|
||||
criteria, err := json.Marshal(&fleet.HostVitalCriteria{
|
||||
Vital: new("end_user_idp_group"),
|
||||
Value: new("Engineering"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a global and a team1-scoped IdP host vitals label.
|
||||
globalLabel, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "idp-cron-global",
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeHostVitals,
|
||||
HostVitalsCriteria: new(json.RawMessage(criteria)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
team1Label, err := ds.NewLabel(ctx, &fleet.Label{
|
||||
Name: "idp-cron-team1",
|
||||
TeamID: &team1.ID,
|
||||
LabelType: fleet.LabelTypeRegular,
|
||||
LabelMembershipType: fleet.LabelMembershipTypeHostVitals,
|
||||
HostVitalsCriteria: new(json.RawMessage(criteria)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Run the actual cron.
|
||||
require.NoError(t, cronHostVitalsLabelMembership(ctx, ds))
|
||||
|
||||
filter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
|
||||
globalHosts, err := ds.ListHostsInLabel(ctx, filter, globalLabel.ID, fleet.HostListOptions{})
|
||||
require.NoError(t, err)
|
||||
gotGlobal := make([]uint, 0, len(globalHosts))
|
||||
for _, h := range globalHosts {
|
||||
gotGlobal = append(gotGlobal, h.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID}, gotGlobal)
|
||||
|
||||
team1Hosts, err := ds.ListHostsInLabel(ctx, filter, team1Label.ID, fleet.HostListOptions{})
|
||||
require.NoError(t, err)
|
||||
gotTeam1 := make([]uint, 0, len(team1Hosts))
|
||||
for _, h := range team1Hosts {
|
||||
gotTeam1 = append(gotTeam1, h.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []uint{hosts[0].ID}, gotTeam1)
|
||||
}
|
||||
|
||||
@@ -1315,6 +1315,12 @@ func TestHostVitalsLabelMembershipJob(t *testing.T) {
|
||||
}
|
||||
|
||||
ds.ListLabelsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions, includeHostCounts bool) ([]*fleet.Label, error) {
|
||||
// The cron must pass a filter that includes fleet/team-scoped labels,
|
||||
// otherwise host vitals labels on a team never get populated (#46869). An
|
||||
// empty TeamFilter (nil User) falls back to global-only labels in
|
||||
// applyLabelTeamFilter, so require a global-admin user here.
|
||||
require.NotNil(t, filter.User, "cron must pass a user-scoped filter so team labels are included")
|
||||
require.True(t, filter.User.HasAnyGlobalRole(), "cron must use a global-admin filter to see all team labels")
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user