Fix bug in MDM command listing (#32992)

Fixes #32996

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## 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
* Corrected team filtering when listing MDM commands to ensure accurate
results. Team-scoped and global commands now display correctly for users
with appropriate access, resolving cases of missing or incorrect entries
when filtering by team.

* Tests
* Added comprehensive coverage for team-scoped MDM command listings,
role-based visibility (team users vs. admins), and hostname ordering to
prevent regressions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky 2025-09-15 15:12:03 -05:00 committed by GitHub
parent ed3e755641
commit c3d73d26a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 103 additions and 1 deletions

View file

@ -0,0 +1 @@
* Fixed 422 error when hitting `/api/v1/fleet/commands` endpoint with team filter.

View file

@ -99,7 +99,7 @@ func (ds *Datastore) ListMDMCommands(
}
jointStmt, params := getCombinedMDMCommandsQuery(ds, listOpts.Filters.HostIdentifier)
jointStmt += ds.whereFilterHostsByTeams(tmFilter, "h")
jointStmt += ds.whereFilterHostsByTeams(tmFilter, "combined_commands")
jointStmt, params = addRequestTypeFilter(jointStmt, &listOpts.Filters, params)
jointStmt, params = appendListOptionsWithCursorToSQL(jointStmt, params, &listOpts.ListOptions)
var results []*fleet.MDMCommand

View file

@ -33,6 +33,7 @@ func TestMDMShared(t *testing.T) {
fn func(t *testing.T, ds *Datastore)
}{
{"TestMDMCommands", testMDMCommands},
{"TestListMDMCommandsWithTeamFilter", testListMDMCommandsWithTeamFilter},
{"TestBatchSetMDMProfiles", testBatchSetMDMProfiles},
{"TestListMDMConfigProfiles", testListMDMConfigProfiles},
{"TestBulkSetPendingMDMHostProfiles", testBulkSetPendingMDMHostProfiles},
@ -386,6 +387,106 @@ func testMDMCommands(t *testing.T, ds *Datastore) {
require.Equal(t, appleCmdUUID2, cmds[0].CommandUUID)
}
// testListMDMCommandsWithTeamFilter tests listing MDM commands with team filters
// This specifically tests the regression where h.team_id was referenced but the query uses combined_commands alias
func testListMDMCommandsWithTeamFilter(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test-team-mdm-commands"})
require.NoError(t, err)
// Create a host in the team
teamHost, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "team-host",
OsqueryHostID: ptr.String("osquery-team"),
NodeKey: ptr.String("node-key-team"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "789012",
TeamID: &team.ID,
})
require.NoError(t, err)
nanoEnroll(t, ds, teamHost, false)
// Create a command for the team host
teamCmdUUID := uuid.New().String()
teamCmd := createRawAppleCmd("ProfileList", teamCmdUUID)
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
err = commander.EnqueueCommand(ctx, []string{teamHost.UUID}, teamCmd)
require.NoError(t, err)
// Create a user with team access
teamUser := &fleet.User{
ID: 999,
Email: "team-user@example.com",
Teams: []fleet.UserTeam{{Team: *team, Role: fleet.RoleMaintainer}},
}
// List commands with team filter (no host identifier) - this would trigger the bug hit by customer
cmds, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: teamUser},
&fleet.MDMCommandListOptions{},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, teamCmdUUID, cmds[0].CommandUUID)
// Test with a specific team filter
cmds, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: teamUser, TeamID: &team.ID},
&fleet.MDMCommandListOptions{},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, teamCmdUUID, cmds[0].CommandUUID)
// Create a host in no team
globalHost, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "global-host",
OsqueryHostID: ptr.String("osquery-global"),
NodeKey: ptr.String("node-key-global"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "789013",
})
require.NoError(t, err)
nanoEnroll(t, ds, globalHost, false)
// Create a command for the global host
globalCmdUUID := uuid.New().String()
globalCmd := createRawAppleCmd("ProfileList", globalCmdUUID)
err = commander.EnqueueCommand(ctx, []string{globalHost.UUID}, globalCmd)
require.NoError(t, err)
// Team user should only see team command
cmds, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: teamUser},
&fleet.MDMCommandListOptions{},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, teamCmdUUID, cmds[0].CommandUUID)
// Test with admin user (should see all commands)
adminUser := test.UserAdmin
cmds, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: adminUser},
&fleet.MDMCommandListOptions{},
)
require.NoError(t, err)
require.Len(t, cmds, 2)
var got []string
for _, cmd := range cmds {
got = append(got, cmd.CommandUUID)
}
require.ElementsMatch(t, []string{teamCmdUUID, globalCmdUUID}, got)
}
func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()