Files
Victor Lyuboslavsky e42dc7accf Improved the performance of the configuration profiles status summary (#48873)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48340 

Windows only. The fix is to use a rollup status table instead of
recalculating the host profile summary on demand.

Verified the fix in load test with 100k Windows MDM hosts. Note that
this does not improve the host details page filtered by OS settings,
which will be handled by the follow up
https://github.com/fleetdm/fleet/issues/48996

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [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.

- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved Windows fleet configuration profile status summaries to avoid
timeouts on large fleets.
* Kept per-host Windows profile statuses accurate after profile updates,
resends, certificate changes, cleanup, unenrollment, and host deletion.
* Added automatic reconciliation to correct stale or orphaned status
data.
* **Data Integrity**
* Improved Windows profile status reporting, including profile and
BitLocker summaries, for more reliable results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 11:59:37 -05:00

5918 lines
218 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mysql
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"database/sql"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/certauth"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMDMShared(t *testing.T) {
ds := CreateMySQLDS(t)
TruncateTables(t, ds)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"TestMDMCommands", testMDMCommands},
{"TestListMDMCommandsWithTeamFilter", testListMDMCommandsWithTeamFilter},
{"TestListMDMCommandsOrderKeys", testListMDMCommandsOrderKeys},
{"TestListMDMAppleCommandsOrderKeys", testListMDMAppleCommandsOrderKeys},
{"TestListMDMCommandsRequiresPerPage", testListMDMCommandsRequiresPerPage},
{"TestListMDMCommandsPagination", testListMDMCommandsPagination},
{"TestBatchSetMDMProfiles", testBatchSetMDMProfiles},
{"TestBatchSetMDMProfilesSoftwareUpdateTracking", testBatchSetMDMProfilesSoftwareUpdateTracking},
{"TestListMDMConfigProfiles", testListMDMConfigProfiles},
{"TestGetHostMDMProfilesExpectedForVerification", testGetHostMDMProfilesExpectedForVerification},
{"TestBatchSetProfileLabelAssociations", testBatchSetProfileLabelAssociations},
{"TestMDMEULA", testMDMEULA},
{"TestGetHostCertAssociationsToExpire", testSCEPRenewalHelpers},
{"TestSCEPRenewalHelpers", testSCEPRenewalHelpers},
{"TestMDMProfilesSummaryAndHostFilters", testMDMProfilesSummaryAndHostFilters},
{"TestIsHostConnectedToFleetMDM", testIsHostConnectedToFleetMDM},
{"TestAreHostsConnectedToFleetMDM", testAreHostsConnectedToFleetMDM},
{"TestBulkSetPendingMDMHostProfilesExcludeAny", testBulkSetPendingMDMHostProfilesExcludeAny},
{"TestBatchResendProfileToHosts", testBatchResendProfileToHosts},
{"TestGetMDMConfigProfileStatus", testGetMDMConfigProfileStatus},
{"TestDeleteMDMProfilesCancelsInstalls", testDeleteMDMProfilesCancelsInstalls},
{"TestDeleteTeamCancelsWindowsProfileInstalls", testDeleteTeamCancelsWindowsProfileInstalls},
{"TestBulkSetPendingDefersWindowsReconciliation", testBulkSetPendingDefersWindowsReconciliation},
{"TestCleanUpMDMManagedCertificates", testCleanUpMDMManagedCertificates},
{"TestEnqueueCommandWithName", testEnqueueCommandWithName},
{"TestProfileHasACMEPayloadForCommand", testProfileHasACMEPayloadForCommand},
{"TestOktaCACleanupTargetForInstallCommand", testOktaCACleanupTargetForInstallCommand},
{"TestRenewMDMManagedCertificatesNullType", testRenewMDMManagedCertificatesNullType},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testMDMCommands(t *testing.T, ds *Datastore) {
ctx := context.Background()
// no commands or devices enrolled => no results
cmds, _, _, err := ds.ListMDMCommands(ctx, fleet.TeamFilter{}, &fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}})
require.NoError(t, err)
require.Empty(t, cmds)
// enroll a windows device
windowsH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host", // ambiguous hostname shared with macOS host
OsqueryHostID: ptr.String("osquery-windows"),
NodeKey: ptr.String("node-key-windows"),
UUID: uuid.NewString(),
Platform: "windows",
HardwareSerial: "123456",
})
require.NoError(t, err)
windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: uuid.New().String(),
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: windowsH.UUID,
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
require.NoError(t, err)
windowsEnrollment.ID = mdmWindowsEnrollmentIDByHardwareID(ctx, t, ds, windowsEnrollment.MDMHardwareID)
_, err = ds.UpdateMDMWindowsEnrollmentsHostUUID(
ctx,
windowsEnrollment.HostUUID,
windowsEnrollment.MDMDeviceID,
)
require.NoError(t, err)
windowsEnrollment, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(
ctx,
windowsEnrollment.MDMDeviceID,
)
require.NoError(t, err)
// enroll a macOS device
macH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host", // ambiguous hostname shared with windows host
OsqueryHostID: ptr.String("osquery-macos"),
NodeKey: ptr.String("node-key-macos"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "654321",
})
require.NoError(t, err)
nanoEnroll(t, ds, macH, false)
// no commands => no results
cmds, _, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}},
)
require.NoError(t, err)
require.Empty(t, cmds)
// insert a windows command
winCmd := &fleet.MDMWindowsCommand{
CommandUUID: uuid.NewString(),
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri",
}
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{windowsEnrollment.MDMDeviceID}, winCmd)
require.NoError(t, err)
// we get one result
cmds, total, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, winCmd.CommandUUID, cmds[0].CommandUUID)
require.Equal(t, winCmd.TargetLocURI, cmds[0].RequestType)
require.Equal(t, "101", cmds[0].Status)
require.Nil(t, total)
appleCmdUUID := uuid.New().String()
appleCmd := createRawAppleCmd("ProfileList", appleCmdUUID)
commander, appleCommanderStorage := createMDMAppleCommanderAndStorage(t, ds)
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, appleCmd)
require.NoError(t, err)
// we get both commands
cmds, total, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "hostname", PerPage: 100},
})
require.NoError(t, err)
require.Len(t, cmds, 2)
require.Equal(t, appleCmdUUID, cmds[0].CommandUUID)
require.Equal(t, "ProfileList", cmds[0].RequestType)
require.Equal(t, "Pending", cmds[0].Status)
require.Equal(t, winCmd.CommandUUID, cmds[1].CommandUUID)
require.Equal(t, winCmd.TargetLocURI, cmds[1].RequestType)
require.Equal(t, "101", cmds[1].Status)
require.Nil(t, total)
// store results for both commands
err = appleCommanderStorage.StoreCommandReport(&mdm.Request{
EnrollID: &mdm.EnrollID{ID: macH.UUID},
Context: ctx,
}, &mdm.CommandResults{
CommandUUID: appleCmdUUID,
Status: "Acknowledged",
Raw: []byte(appleCmd),
})
require.NoError(t, err)
_, err = ds.MDMWindowsSaveResponse(ctx, windowsEnrollment, fleet.EnrichedSyncML{
SyncML: &fleet.SyncML{
Raw: []byte("<xml></xml>"),
},
CmdRefUUIDToStatus: map[string]fleet.SyncMLCmd{winCmd.CommandUUID: {
Data: ptr.String("200"),
}},
CmdRefUUIDs: []string{winCmd.CommandUUID},
}, []string{})
require.NoError(t, err)
// we get both commands
cmds, total, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "hostname", PerPage: 100},
})
require.NoError(t, err)
require.Len(t, cmds, 2)
require.Equal(t, appleCmdUUID, cmds[0].CommandUUID)
require.Equal(t, "ProfileList", cmds[0].RequestType)
require.Equal(t, "Acknowledged", cmds[0].Status)
require.Equal(t, winCmd.CommandUUID, cmds[1].CommandUUID)
require.Equal(t, winCmd.TargetLocURI, cmds[1].RequestType)
require.Equal(t, "200", cmds[1].Status)
require.Nil(t, total)
// add more windows commands
winCmd2 := &fleet.MDMWindowsCommand{
CommandUUID: uuid.NewString(),
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri2",
}
winCmd3 := &fleet.MDMWindowsCommand{
CommandUUID: uuid.NewString(),
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri3",
}
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{windowsEnrollment.MDMDeviceID}, winCmd2)
require.NoError(t, err)
err = ds.MDMWindowsInsertCommandForHosts(ctx, []string{windowsEnrollment.MDMDeviceID}, winCmd3)
require.NoError(t, err)
// add more macos commands
appleCmdUUID2 := uuid.New().String()
appleCmd2 := createRawAppleCmd("InstallProfile", appleCmdUUID2)
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, appleCmd2)
require.NoError(t, err)
appleCmdUUID3 := uuid.New().String()
appleCmd3 := createRawAppleCmd("RemoveProfile", appleCmdUUID3)
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, appleCmd3)
require.NoError(t, err)
// non-existent host identifier
cmds, _, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
HostIdentifier: "non-existent",
},
},
)
require.NoError(t, err)
require.Len(t, cmds, 0)
// non-existent request type
cmds, _, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
RequestType: "non-existent",
},
},
)
require.NoError(t, err)
require.Len(t, cmds, 0)
for _, tc := range []struct {
name string
identifier string
commandStatus *fleet.MDMCommandStatusFilter
expected []string
requestType string
}{
{
name: "windows host by hostname ambiguous with macOS host",
identifier: windowsH.Hostname,
expected: []string{
winCmd.CommandUUID, winCmd2.CommandUUID, winCmd3.CommandUUID,
appleCmdUUID, appleCmdUUID2, appleCmdUUID3,
},
},
{
name: "windows host by UUID",
identifier: windowsH.UUID,
expected: []string{winCmd.CommandUUID, winCmd2.CommandUUID, winCmd3.CommandUUID},
},
{
name: "windows host by UUID, filter by request type",
identifier: windowsH.UUID,
expected: []string{winCmd2.CommandUUID},
requestType: "./test/uri2",
},
{
name: "windows host by hardware serial",
identifier: windowsH.HardwareSerial,
expected: []string{winCmd.CommandUUID, winCmd2.CommandUUID, winCmd3.CommandUUID},
},
{
name: "macOS host by hostname ambiguous with windows host",
identifier: macH.Hostname,
expected: []string{
appleCmdUUID, appleCmdUUID2, appleCmdUUID3,
winCmd.CommandUUID, winCmd2.CommandUUID, winCmd3.CommandUUID,
},
},
{
name: "macOS host by UUID",
identifier: macH.UUID,
commandStatus: new(fleet.MDMCommandStatusFilterPending),
expected: []string{appleCmdUUID2, appleCmdUUID3},
},
{
name: "macOS host by hardware serial",
identifier: macH.HardwareSerial,
expected: []string{appleCmdUUID, appleCmdUUID2, appleCmdUUID3},
},
} {
t.Run(tc.name, func(t *testing.T) {
commandStatuses := []fleet.MDMCommandStatusFilter{}
if tc.commandStatus != nil {
commandStatuses = append(commandStatuses, *tc.commandStatus)
}
cmds, total, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
HostIdentifier: tc.identifier,
CommandStatuses: commandStatuses,
RequestType: tc.requestType,
},
},
)
require.NoError(t, err)
require.Len(t, cmds, len(tc.expected))
var got []string
for _, cmd := range cmds {
got = append(got, cmd.CommandUUID)
}
require.ElementsMatch(t, tc.expected, got)
if tc.commandStatus != nil && *tc.commandStatus == fleet.MDMCommandStatusFilterPending {
require.Equal(t, int64(len(tc.expected)), *total)
} else {
require.Nil(t, total)
}
})
}
// add macos host
macH2, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-2",
OsqueryHostID: ptr.String("osquery-macos-2"),
NodeKey: ptr.String("node-key-macos-2"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "654322",
})
require.NoError(t, err)
nanoEnroll(t, ds, macH2, false)
// add more macos commands
appleCmdUUID4 := uuid.New().String()
appleCmd4 := createRawAppleCmd("InstallProfile", appleCmdUUID4)
err = commander.EnqueueCommand(ctx, []string{macH2.UUID}, appleCmd4)
require.NoError(t, err)
// filter by request_type
cmds, _, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "hostname", OrderDirection: fleet.OrderAscending, PerPage: 100},
Filters: fleet.MDMCommandFilters{
RequestType: "InstallProfile",
},
},
)
require.NoError(t, err)
require.Len(t, cmds, 2)
require.Equal(t, appleCmdUUID4, cmds[0].CommandUUID)
require.Equal(t, appleCmdUUID2, cmds[1].CommandUUID)
// filter by request_type and host_identifier
cmds, _, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
RequestType: "InstallProfile",
HostIdentifier: macH.UUID,
},
},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
require.Equal(t, appleCmdUUID2, cmds[0].CommandUUID)
// filter by command status for windows host
cmds, total, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
HostIdentifier: "123456",
CommandStatuses: []fleet.MDMCommandStatusFilter{fleet.MDMCommandStatusFilterPending},
},
},
)
require.Error(t, err)
require.ErrorContains(t, err, `Currently, "command_status" filter is only available for macOS, iOS, and iPadOS hosts.`)
require.Nil(t, cmds)
require.Nil(t, total)
failedAppleCmdUUID := uuid.New().String()
appleCmd = createRawAppleCmd("ProfileList", failedAppleCmdUUID)
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, appleCmd)
require.NoError(t, err)
// store failed result for the command
err = appleCommanderStorage.StoreCommandReport(&mdm.Request{
EnrollID: &mdm.EnrollID{ID: macH.UUID},
Context: ctx,
}, &mdm.CommandResults{
CommandUUID: failedAppleCmdUUID,
Status: "Error",
Raw: []byte(appleCmd),
})
require.NoError(t, err)
// Update the timestamp to ensure there is enough difference for ordering to have it always come first
// using default list options
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(
ctx,
`UPDATE nano_command_results SET updated_at = updated_at + INTERVAL 1 SECOND WHERE command_uuid = ?`,
failedAppleCmdUUID,
)
return err
})
cmds, total, _, err = ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: fleet.MDMCommandFilters{
HostIdentifier: macH.UUID,
CommandStatuses: []fleet.MDMCommandStatusFilter{fleet.MDMCommandStatusFilterRan, fleet.MDMCommandStatusFilterFailed},
},
},
)
require.NoError(t, err)
require.Nil(t, total)
require.Len(t, cmds, 2)
var got []string
for _, cmd := range cmds {
got = append(got, cmd.CommandUUID)
}
require.Equal(t, []string{failedAppleCmdUUID, appleCmdUUID}, got)
// pagination and pagination meta data
cmds, _, meta, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{Page: 0, PerPage: 1, IncludeMetadata: true},
Filters: fleet.MDMCommandFilters{
HostIdentifier: macH.UUID,
CommandStatuses: []fleet.MDMCommandStatusFilter{fleet.MDMCommandStatusFilterRan, fleet.MDMCommandStatusFilterFailed},
},
},
)
require.Len(t, cmds, 1)
require.NoError(t, err)
require.Equal(t, true, meta.HasNextResults)
require.Equal(t, false, meta.HasPreviousResults)
// Cursor pagination on the host-scoped path. Regression test for
// https://github.com/fleetdm/fleet/issues/44422:
// "Error 1052 (23000): Column '<col>' in where clause is ambiguous".
// Covers all three Path A SQL shapes: Apple-only, Windows-only, and
// multi-platform (host_identifier resolves to both an Apple and a
// Windows host via the shared hostname "test-host").
t.Run("cursor_pagination_44422", func(t *testing.T) {
for _, tc := range []struct {
name string
hostIdentifier string
wantPlatforms []string
}{
{"apple_only", macH.UUID, []string{"darwin"}},
{"multi_platform", "test-host", []string{"darwin", "windows"}},
{"windows_only", windowsH.UUID, []string{"windows"}},
} {
t.Run(tc.name, func(t *testing.T) {
filters := fleet.MDMCommandFilters{HostIdentifier: tc.hostIdentifier}
// Sanity: confirm the dispatch reaches the expected branch(es)
// and there is data to paginate over.
all, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{PerPage: 100},
Filters: filters,
},
)
require.NoError(t, err)
require.NotEmpty(t, all)
platforms := map[string]struct{}{}
for _, c := range all {
switch c.HostUUID {
case macH.UUID:
platforms["darwin"] = struct{}{}
case windowsH.UUID:
platforms["windows"] = struct{}{}
}
}
for _, p := range tc.wantPlatforms {
_, ok := platforms[p]
require.True(t, ok, "expected commands from platform %s", p)
}
// Cursor on updated_at — `updated_at` is exposed by multiple
// inner-FROM tables on every Path A branch, so this would fail
// with "Column 'updated_at' ... is ambiguous" without the wrap.
page1, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 1,
OrderKey: "updated_at",
OrderDirection: fleet.OrderDescending,
},
Filters: filters,
},
)
require.NoError(t, err)
require.Len(t, page1, 1)
page2, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 1,
OrderKey: "updated_at",
OrderDirection: fleet.OrderDescending,
After: page1[0].UpdatedAt.Format(time.RFC3339Nano),
},
Filters: filters,
},
)
// require.NoError is the regression guard. The cursor predicate
// is strict (`updated_at < ?`, no tiebreaker), so page2 may be
// empty when adjacent commands share an updated_at second — that
// is expected, not a regression. Only assert distinctness when
// a row did come back.
require.NoError(t, err)
if len(page2) > 0 {
require.NotEqual(t, page1[0].CommandUUID, page2[0].CommandUUID)
}
// Cursor on command_uuid — separate ambiguity manifestation;
// `command_uuid` is the join column on every inner table.
first, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 1,
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
},
Filters: filters,
},
)
require.NoError(t, err)
require.Len(t, first, 1)
next, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 1,
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
After: first[0].CommandUUID,
},
Filters: filters,
},
)
require.NoError(t, err)
require.NotEmpty(t, next)
require.NotEqual(t, first[0].CommandUUID, next[0].CommandUUID)
// Cursor on hostname. All hosts in this test share hostname
// "test-host", and the helper's cursor predicate is a strict
// `hostname > 'test-host'` (no tiebreaker), so the result is
// expected to be empty. The bug would surface as a SQL error
// before the empty check.
afterHostname, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 5,
OrderKey: "hostname",
OrderDirection: fleet.OrderAscending,
After: "test-host",
},
Filters: filters,
},
)
require.NoError(t, err)
require.Empty(t, afterHostname)
// Pending-only count + cursor
if tc.name == "apple_only" {
pendingFilters := fleet.MDMCommandFilters{
HostIdentifier: tc.hostIdentifier,
CommandStatuses: []fleet.MDMCommandStatusFilter{fleet.MDMCommandStatusFilterPending},
}
// Establish the unfiltered pending baseline so the post-cursor
// assertions don't depend on UUID lexicographic ordering.
allPending, totalPending, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 100,
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
IncludeMetadata: true,
},
Filters: pendingFilters,
},
)
require.NoError(t, err)
require.NotNil(t, totalPending)
require.Equal(t, int64(len(allPending)), *totalPending)
require.Greater(t, len(allPending), 1, "test setup expects multiple pending Apple commands")
// Drive the cursor from a known pending UUID so the post-cursor
// expectation is deterministic.
cursorUUID := allPending[0].CommandUUID
expectedAfter := allPending[1:]
afterPending, totalAfter, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
PerPage: 100,
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
After: cursorUUID,
IncludeMetadata: true,
},
Filters: pendingFilters,
},
)
require.NoError(t, err)
// total comes from countStmt which has no cursor, so it
// must report the full pending count regardless of After.
require.NotNil(t, totalAfter)
require.Equal(t, *totalPending, *totalAfter)
require.Len(t, afterPending, len(expectedAfter))
gotUUIDs := make([]string, len(afterPending))
for i, c := range afterPending {
gotUUIDs[i] = c.CommandUUID
}
expectedUUIDs := make([]string, len(expectedAfter))
for i, c := range expectedAfter {
expectedUUIDs[i] = c.CommandUUID
}
require.Equal(t, expectedUUIDs, gotUUIDs)
}
})
}
})
}
// 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{ListOptions: fleet.ListOptions{PerPage: 100}},
)
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{ListOptions: fleet.ListOptions{PerPage: 100}},
)
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{ListOptions: fleet.ListOptions{PerPage: 100}},
)
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{ListOptions: fleet.ListOptions{PerPage: 100}},
)
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 testListMDMCommandsRequiresPerPage(t *testing.T, ds *Datastore) {
ctx := t.Context()
_, _, _, err := ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "PerPage")
_, _, _, err = ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "PerPage")
}
// testListMDMCommandsPagination exercises the all-hosts per-branch
// pagination across both Apple and Windows branches. The path inflates the
// inner LIMIT to per_page*page+per_page so that page N is correct even if all
// matching rows came from a single branch, and the outer wrap clears
// After/Page when cursor pagination is used.
func testListMDMCommandsPagination(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Enroll one Windows host and one macOS host.
winHost := test.NewHost(t, ds, "paginate-win", "1.2.3.4", "paginate-node-win", uuid.NewString(), time.Now(), test.WithPlatform("windows"))
winDeviceID := windowsEnroll(t, ds, winHost)
winEnrollment, err := ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, winDeviceID)
require.NoError(t, err)
macHost := test.NewHost(t, ds, "paginate-mac", "1.2.3.5", "paginate-node-mac", uuid.NewString(), time.Now())
nanoEnroll(t, ds, macHost, false)
// Insert 3 Apple commands and 3 Windows commands so both branches contribute.
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
const totalApple = 3
const totalWin = 3
allUUIDs := make([]string, 0, totalApple+totalWin)
for range totalApple {
cmdUUID := uuid.NewString()
raw := createRawAppleCmd("ProfileList", cmdUUID)
require.NoError(t, commander.EnqueueCommand(ctx, []string{macHost.UUID}, raw))
allUUIDs = append(allUUIDs, cmdUUID)
}
winUUIDs := make([]string, 0, totalWin)
for range totalWin {
cmdUUID := uuid.NewString()
require.NoError(t, ds.MDMWindowsInsertCommandForHosts(ctx, []string{winDeviceID}, &fleet.MDMWindowsCommand{
CommandUUID: cmdUUID,
RawCommand: []byte("<Exec></Exec>"),
TargetLocURI: "./test/uri",
}))
winUUIDs = append(winUUIDs, cmdUUID)
allUUIDs = append(allUUIDs, cmdUUID)
}
// Mark one Windows command as responded so the results-backed branch of the
// internal Windows UNION ALL is exercised. The dedupe NOT EXISTS clause
// must still keep the command from appearing twice across the pagination.
respondedWinUUID := winUUIDs[0]
_, err = ds.MDMWindowsSaveResponse(ctx, winEnrollment, fleet.EnrichedSyncML{
SyncML: &fleet.SyncML{Raw: []byte("<xml></xml>")},
CmdRefUUIDToStatus: map[string]fleet.SyncMLCmd{
respondedWinUUID: {Data: ptr.String("200")},
},
CmdRefUUIDs: []string{respondedWinUUID},
}, []string{})
require.NoError(t, err)
sort.Strings(allUUIDs)
totalCount := len(allUUIDs)
t.Run("page-based across branches", func(t *testing.T) {
// PerPage=2 across 6 commands => 3 pages of 2 each.
const perPage = 2
seen := make(map[string]bool)
for page := uint(0); page*perPage < uint(totalCount); page++ { //nolint:gosec
cmds, _, meta, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
Page: page,
PerPage: perPage,
IncludeMetadata: true,
},
},
)
require.NoError(t, err)
require.LessOrEqual(t, len(cmds), perPage)
start := int(page * perPage)
end := min(start+perPage, totalCount)
expected := allUUIDs[start:end]
got := make([]string, 0, len(cmds))
for _, c := range cmds {
require.False(t, seen[c.CommandUUID], "duplicate command UUID across pages: %s", c.CommandUUID)
seen[c.CommandUUID] = true
got = append(got, c.CommandUUID)
}
require.Equal(t, expected, got, "page %d", page)
require.Equal(t, page > 0, meta.HasPreviousResults, "page %d HasPreviousResults", page)
require.Equal(t, end < totalCount, meta.HasNextResults, "page %d HasNextResults", page)
}
require.Len(t, seen, totalCount)
})
t.Run("cursor walks all rows without overlap", func(t *testing.T) {
const perPage = 2
seen := make(map[string]bool)
var after string
for {
cmds, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
PerPage: perPage,
After: after,
IncludeMetadata: true,
},
},
)
require.NoError(t, err)
if len(cmds) == 0 {
break
}
for _, c := range cmds {
require.False(t, seen[c.CommandUUID], "duplicate command UUID across cursor pages: %s", c.CommandUUID)
seen[c.CommandUUID] = true
}
after = cmds[len(cmds)-1].CommandUUID
if len(cmds) < perPage {
break
}
}
require.Len(t, seen, totalCount)
})
t.Run("outer clears Page when After is set", func(t *testing.T) {
// Without the outer-wrap clearing of Page, Page=99 with PerPage=2 would
// add OFFSET 198 on top of the cursor filter and return zero rows even
// though plenty of rows follow allUUIDs[0]. With the clearing, the call
// must return the page-sized slice immediately after the cursor.
cmds, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "command_uuid",
OrderDirection: fleet.OrderAscending,
PerPage: 2,
Page: 99,
After: allUUIDs[0],
IncludeMetadata: true,
},
},
)
require.NoError(t, err)
require.Equal(t, allUUIDs[1:3], extractCommandUUIDs(cmds))
})
}
func extractCommandUUIDs(cmds []*fleet.MDMCommand) []string {
out := make([]string, 0, len(cmds))
for _, c := range cmds {
out = append(out, c.CommandUUID)
}
return out
}
func testListMDMCommandsOrderKeys(t *testing.T, ds *Datastore) {
ctx := t.Context()
macH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "ord-host",
OsqueryHostID: ptr.String("ord-osq"),
NodeKey: ptr.String("ord-nk"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "ORDABC",
})
require.NoError(t, err)
nanoEnroll(t, ds, macH, false)
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
for range 3 {
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, createRawAppleCmd("ProfileList", uuid.NewString()))
require.NoError(t, err)
}
for _, key := range []string{"command_uuid", "request_type", "status", "updated_at", "hostname", "host_uuid", "name"} {
t.Run("order_"+key, func(t *testing.T) {
cmds, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: key, PerPage: 5},
},
)
require.NoError(t, err)
require.Len(t, cmds, 3)
})
}
t.Run("rejects_unknown_key", func(t *testing.T) {
_, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "not_a_real_column", PerPage: 5},
},
)
require.Error(t, err)
var invalidKeyErr common_mysql.InvalidOrderKeyError
require.ErrorAs(t, err, &invalidKeyErr)
})
// the host-identifier branch uses a separate query; confirm it shares the allowlist
t.Run("rejects_unknown_key_host_identifier", func(t *testing.T) {
_, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "not_a_real_column", PerPage: 5},
Filters: fleet.MDMCommandFilters{HostIdentifier: macH.UUID},
},
)
require.Error(t, err)
var invalidKeyErr common_mysql.InvalidOrderKeyError
require.ErrorAs(t, err, &invalidKeyErr)
})
t.Run("after_pagination_with_allowed_key", func(t *testing.T) {
cmds, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1, IncludeMetadata: true},
},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
afterCursor := cmds[0].CommandUUID
next, _, _, err := ds.ListMDMCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1, After: afterCursor},
},
)
require.NoError(t, err)
require.Len(t, next, 1)
require.NotEqual(t, afterCursor, next[0].CommandUUID)
})
}
func testListMDMAppleCommandsOrderKeys(t *testing.T, ds *Datastore) {
ctx := t.Context()
macH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "ord-apple-host",
OsqueryHostID: ptr.String("ord-apple-osq"),
NodeKey: ptr.String("ord-apple-nk"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "ORDA1",
})
require.NoError(t, err)
nanoEnroll(t, ds, macH, false)
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
for range 2 {
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, createRawAppleCmd("ProfileList", uuid.NewString()))
require.NoError(t, err)
}
for _, key := range []string{"command_uuid", "request_type", "status", "updated_at", "hostname", "device_id"} {
t.Run("order_"+key, func(t *testing.T) {
cmds, err := ds.ListMDMAppleCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: key, PerPage: 5},
},
)
require.NoError(t, err)
require.Len(t, cmds, 2)
})
}
t.Run("rejects_unknown_key", func(t *testing.T) {
_, err := ds.ListMDMAppleCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "not_a_real_column"},
},
)
require.Error(t, err)
})
t.Run("after_pagination_with_allowed_key", func(t *testing.T) {
cmds, err := ds.ListMDMAppleCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1},
},
)
require.NoError(t, err)
require.Len(t, cmds, 1)
afterCursor := cmds[0].CommandUUID
next, err := ds.ListMDMAppleCommands(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
&fleet.MDMCommandListOptions{
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1, After: afterCursor},
},
)
require.NoError(t, err)
require.Len(t, next, 1)
require.NotEqual(t, afterCursor, next[0].CommandUUID)
})
}
func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
applyAndExpect := func(
newAppleSet []*fleet.MDMAppleConfigProfile,
newWindowsSet []*fleet.MDMWindowsConfigProfile,
newAppleDeclSet []*fleet.MDMAppleDeclaration,
newAndroidSet []*fleet.MDMAndroidConfigProfile,
tmID *uint,
wantApple []*fleet.MDMAppleConfigProfile,
wantWindows []*fleet.MDMWindowsConfigProfile,
wantAppleDecl []*fleet.MDMAppleDeclaration,
wantAndroid []*fleet.MDMAndroidConfigProfile,
wantUpdates fleet.MDMProfilesUpdates,
) {
updates, err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet, newAppleDeclSet, newAndroidSet, nil)
require.NoError(t, err)
expectAppleProfiles(t, ds, tmID, wantApple)
expectWindowsProfiles(t, ds, tmID, wantWindows)
expectAppleDeclarations(t, ds, tmID, wantAppleDecl)
expectAndroidProfiles(t, ds, tmID, wantAndroid)
assert.Equal(t, wantUpdates, updates)
}
withTeamIDApple := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile {
p.TeamID = &tmID
return p
}
withTeamIDDecl := func(d *fleet.MDMAppleDeclaration, tmID uint) *fleet.MDMAppleDeclaration {
d.TeamID = &tmID
return d
}
withTeamIDWindows := func(p *fleet.MDMWindowsConfigProfile, tmID uint) *fleet.MDMWindowsConfigProfile {
p.TeamID = &tmID
return p
}
withTeamIDAndroid := func(p *fleet.MDMAndroidConfigProfile, tmID uint) *fleet.MDMAndroidConfigProfile {
p.TeamID = &tmID
return p
}
// empty set for no team (Apple, Windows and Android)
applyAndExpect(nil, nil, nil, nil, nil, nil, nil, nil, nil, fleet.MDMProfilesUpdates{})
// single Apple, Windows and Android profile set for a specific team
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")},
[]*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")},
[]*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")},
[]*fleet.MDMAndroidConfigProfile{androidConfigProfileForTest(t, "A1", nil)},
ptr.Uint(1),
[]*fleet.MDMAppleConfigProfile{
withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1),
},
[]*fleet.MDMWindowsConfigProfile{
withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1),
},
[]*fleet.MDMAppleDeclaration{withTeamIDDecl(declForTest("D1", "D1", "foo"), 1)},
[]*fleet.MDMAndroidConfigProfile{withTeamIDAndroid(androidConfigProfileForTest(t, "A1", nil), 1)},
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// single Apple, Windows and Android profile set for no team
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")},
[]*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")},
[]*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")},
[]*fleet.MDMAndroidConfigProfile{androidConfigProfileForTest(t, "A1", nil)},
nil,
[]*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")},
[]*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")},
[]*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")},
[]*fleet.MDMAndroidConfigProfile{androidConfigProfileForTest(t, "A1", nil)},
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// new Apple, Windows and Android profile sets for a specific team
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N1", "I1", "a"), // unchanged
configProfileForTest(t, "N2", "I2", "b"),
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W1", "l1"), // unchanged
windowsConfigProfileForTest(t, "W2", "l2"),
},
[]*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo"), // unchanged
declForTest("D2", "D2", "foo"),
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A1", nil), // unchanged
androidConfigProfileForTest(t, "A2", nil),
},
ptr.Uint(1),
[]*fleet.MDMAppleConfigProfile{
withTeamIDApple(configProfileForTest(t, "N1", "I1", "a"), 1),
withTeamIDApple(configProfileForTest(t, "N2", "I2", "b"), 1),
},
[]*fleet.MDMWindowsConfigProfile{
withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1),
withTeamIDWindows(windowsConfigProfileForTest(t, "W2", "l2"), 1),
},
[]*fleet.MDMAppleDeclaration{
withTeamIDDecl(declForTest("D1", "D1", "foo"), 1),
withTeamIDDecl(declForTest("D2", "D2", "foo"), 1),
},
[]*fleet.MDMAndroidConfigProfile{
withTeamIDAndroid(androidConfigProfileForTest(t, "A1", nil), 1),
withTeamIDAndroid(androidConfigProfileForTest(t, "A2", nil), 1),
},
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// edited profiles, unchanged profiles, and new profiles for a specific team
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N1", "I1", "a-updated"), // content updated
configProfileForTest(t, "N2", "I2", "b"), // unchanged
configProfileForTest(t, "N3", "I3", "c"), // new
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W1", "l1-updated"), // content updated
windowsConfigProfileForTest(t, "W2", "l2"), // unchanged
windowsConfigProfileForTest(t, "W3", "l3"), // new
},
[]*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo-updated"), // content updated
declForTest("D2", "D2", "foo"), // unchanged
declForTest("D3", "D3", "bar"), // new
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A1", map[string]any{"cameraDisabled": true}), // Content updated
androidConfigProfileForTest(t, "A2", nil), // unchanged
androidConfigProfileForTest(t, "A3", nil), // new
},
ptr.Uint(1),
[]*fleet.MDMAppleConfigProfile{
withTeamIDApple(configProfileForTest(t, "N1", "I1", "a-updated"), 1),
withTeamIDApple(configProfileForTest(t, "N2", "I2", "b"), 1),
withTeamIDApple(configProfileForTest(t, "N3", "I3", "c"), 1),
},
[]*fleet.MDMWindowsConfigProfile{
withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1-updated"), 1),
withTeamIDWindows(windowsConfigProfileForTest(t, "W2", "l2"), 1),
withTeamIDWindows(windowsConfigProfileForTest(t, "W3", "l3"), 1),
},
[]*fleet.MDMAppleDeclaration{
withTeamIDDecl(declForTest("D1", "D1", "foo-updated"), 1),
withTeamIDDecl(declForTest("D2", "D2", "foo"), 1),
withTeamIDDecl(declForTest("D3", "D3", "bar"), 1),
},
[]*fleet.MDMAndroidConfigProfile{
withTeamIDAndroid(androidConfigProfileForTest(t, "A1", map[string]any{"cameraDisabled": true}), 1),
withTeamIDAndroid(androidConfigProfileForTest(t, "A2", nil), 1),
withTeamIDAndroid(androidConfigProfileForTest(t, "A3", nil), 1),
},
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// new Apple, Windows and Android profiles to no team
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N4", "I4", "d"),
configProfileForTest(t, "N5", "I5", "e"),
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W4", "l4"),
windowsConfigProfileForTest(t, "W5", "l5"),
},
[]*fleet.MDMAppleDeclaration{
declForTest("D5", "D4", "foo"),
declForTest("D4", "D5", "foo"),
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A4", nil),
androidConfigProfileForTest(t, "A5", nil),
},
nil,
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N4", "I4", "d"),
configProfileForTest(t, "N5", "I5", "e"),
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W4", "l4"),
windowsConfigProfileForTest(t, "W5", "l5"),
},
[]*fleet.MDMAppleDeclaration{
declForTest("D5", "D4", "foo"),
declForTest("D4", "D5", "foo"),
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A4", nil),
androidConfigProfileForTest(t, "A5", nil),
},
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// Apply the same profiles again -- no update should be detected
applyAndExpect(
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N4", "I4", "d"),
configProfileForTest(t, "N5", "I5", "e"),
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W4", "l4"),
windowsConfigProfileForTest(t, "W5", "l5"),
},
[]*fleet.MDMAppleDeclaration{
declForTest("D5", "D4", "foo"),
declForTest("D4", "D5", "foo"),
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A4", nil),
androidConfigProfileForTest(t, "A5", nil),
},
nil,
[]*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "N4", "I4", "d"),
configProfileForTest(t, "N5", "I5", "e"),
},
[]*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W4", "l4"),
windowsConfigProfileForTest(t, "W5", "l5"),
},
[]*fleet.MDMAppleDeclaration{
declForTest("D5", "D4", "foo"),
declForTest("D4", "D5", "foo"),
},
[]*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "A4", nil),
androidConfigProfileForTest(t, "A5", nil),
},
fleet.MDMProfilesUpdates{AppleConfigProfile: false, WindowsConfigProfile: false, AppleDeclaration: false, AndroidConfigProfile: false},
)
// Test Case 8: Clear profiles for a specific team
applyAndExpect(nil, nil, nil, nil, ptr.Uint(1), nil, nil, nil, nil,
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
// create some labels to test batch-setting label-scoped declarations
lblExcl, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-label-1", Query: "select 1"})
require.NoError(t, err)
lblExcl2, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-label-2", Query: "select 2"})
require.NoError(t, err)
lblInclAny, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-label-3", Query: "select 3"})
require.NoError(t, err)
lblInclAny2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-label-4", Query: "select 4"})
require.NoError(t, err)
lblInclAll, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclall-label-5", Query: "select 5"})
require.NoError(t, err)
lblInclAll2, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclall-label-6", Query: "select 6"})
require.NoError(t, err)
// we only care about declarations here, as batch-setting labels for profiles
// is tested elsewhere.
applyAndExpect(nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo", lblExcl, lblExcl2),
declForTest("D2", "D2", "foo", lblInclAll, lblInclAll2),
}, nil, nil,
nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo", lblExcl, lblExcl2),
declForTest("D2", "D2", "foo", lblInclAll, lblInclAll2),
}, nil,
// this removed the apple, windows and android profiles for no team, so updated is true
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true, AndroidConfigProfile: true},
)
applyAndExpect(nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo", lblInclAny, lblInclAny2),
declForTest("D2", "D2", "foo"),
}, nil, nil,
nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo", lblInclAny, lblInclAny2),
declForTest("D2", "D2", "foo"),
}, nil,
fleet.MDMProfilesUpdates{AppleConfigProfile: false, WindowsConfigProfile: false, AppleDeclaration: true, AndroidConfigProfile: false},
)
applyAndExpect(nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo"),
}, nil, nil,
nil, nil, []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "foo"),
}, nil,
fleet.MDMProfilesUpdates{AppleConfigProfile: false, WindowsConfigProfile: false, AppleDeclaration: true, AndroidConfigProfile: false},
)
}
func testBatchSetMDMProfilesSoftwareUpdateTracking(t *testing.T, ds *Datastore) {
ctx := t.Context()
winOSUpdate := func(name string) *fleet.MDMWindowsConfigProfile {
return &fleet.MDMWindowsConfigProfile{
Name: name,
SyncML: fmt.Appendf(nil,
`<Replace><Item><Target><LocURI>./Device%s/Install</LocURI></Target></Item></Replace>`,
syncml.FleetOSUpdateTargetLocURI),
}
}
appleOSUpdate := func(name, identifier string) *fleet.MDMAppleDeclaration {
return &fleet.MDMAppleDeclaration{
Name: name,
Identifier: identifier,
RawJSON: json.RawMessage(fmt.Sprintf(`{
"Type": %q,
"Identifier": %q,
"Payload": {"TargetOSVersion": "14.0", "TargetLocalDateTime": "2025-01-01T12:00:00"}
}`, apple_mdm.DeclarationTypeSoftwareUpdate, identifier)),
}
}
// A batch with an OS-update Windows profile and Apple declaration tracks both.
_, err := ds.BatchSetMDMProfiles(ctx, nil, nil,
[]*fleet.MDMWindowsConfigProfile{winOSUpdate("win-os")},
[]*fleet.MDMAppleDeclaration{appleOSUpdate("apple-os", "com.fleet.os")}, nil, nil)
require.NoError(t, err)
winConfigured, err := ds.HasWindowsUpdateConfigProfileConfigured(ctx, 0)
require.NoError(t, err)
require.True(t, winConfigured)
appleConfigured, err := ds.HasAppleUpdateConfigProfileConfigured(ctx, 0)
require.NoError(t, err)
require.True(t, appleConfigured)
// Re-applying a batch without the OS-update profiles deletes them, and the
// tracking rows are cleared via ON DELETE CASCADE.
_, err = ds.BatchSetMDMProfiles(ctx, nil, nil,
[]*fleet.MDMWindowsConfigProfile{{Name: "other", SyncML: []byte("<Replace></Replace>")}},
nil, nil, nil)
require.NoError(t, err)
winConfigured, err = ds.HasWindowsUpdateConfigProfileConfigured(ctx, 0)
require.NoError(t, err)
require.False(t, winConfigured)
appleConfigured, err = ds.HasAppleUpdateConfigProfileConfigured(ctx, 0)
require.NoError(t, err)
require.False(t, appleConfigured)
}
// simulateBrokenLabel nulls label_id in profile and declaration label tables for the
// given label name, simulating the broken-label state that previously occurred automatically
// via ON DELETE SET NULL (now ON DELETE RESTRICT prevents label deletion while referenced).
func simulateBrokenLabel(t *testing.T, ds *Datastore, ctx context.Context, labelName string) {
t.Helper()
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_name = ?`, labelName); err != nil {
return err
}
_, err := tx.ExecContext(ctx, `UPDATE mdm_declaration_labels SET label_id = NULL WHERE label_name = ?`, labelName)
return err
})
}
func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
opts := fleet.ListOptions{OrderKey: "name", IncludeMetadata: true}
winProf := []byte("<Replace></Replace>")
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
require.NoError(t, err)
// both profile tables are empty
profs, meta, err := ds.ListMDMConfigProfiles(ctx, nil, opts)
require.NoError(t, err)
require.Len(t, profs, 0)
require.Equal(t, *meta, fleet.PaginationMetadata{})
// add fleet-managed Apple profiles for the team and globally
for idf := range mobileconfig.FleetPayloadIdentifiers() {
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name_"+idf, idf, team.ID), nil)
require.NoError(t, err)
_, err = ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("name_"+idf, idf, 0), nil)
require.NoError(t, err)
}
// still returns no result
profs, meta, err = ds.ListMDMConfigProfiles(ctx, nil, opts)
require.NoError(t, err)
require.Len(t, profs, 0)
require.Equal(t, *meta, fleet.PaginationMetadata{})
profs, meta, err = ds.ListMDMConfigProfiles(ctx, &team.ID, opts)
require.NoError(t, err)
require.Len(t, profs, 0)
require.Equal(t, *meta, fleet.PaginationMetadata{})
// add fleet-managed Windows profiles for the team and globally
for name := range mdm_types.FleetReservedProfileNames() {
_, err = ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{Name: name, TeamID: &team.ID, SyncML: winProf},
nil,
)
require.NoError(t, err)
_, err = ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{Name: name, TeamID: nil, SyncML: winProf},
nil,
)
require.NoError(t, err)
}
// still returns no result
profs, meta, err = ds.ListMDMConfigProfiles(ctx, nil, opts)
require.NoError(t, err)
require.Len(t, profs, 0)
require.Equal(t, *meta, fleet.PaginationMetadata{})
profs, meta, err = ds.ListMDMConfigProfiles(ctx, &team.ID, opts)
require.NoError(t, err)
require.Len(t, profs, 0)
require.Equal(t, *meta, fleet.PaginationMetadata{})
// create a mac profile for global and a Windows profile for team
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("A", "A", 0), nil)
require.NoError(t, err)
profB, err := ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{Name: "B", TeamID: &team.ID, SyncML: winProf},
nil,
)
require.NoError(t, err)
// get global profiles returns the mac one
profs, meta, err = ds.ListMDMConfigProfiles(ctx, nil, opts)
require.NoError(t, err)
require.Len(t, profs, 1)
require.Equal(t, profA.Name, profs[0].Name)
require.Equal(t, *meta, fleet.PaginationMetadata{})
// get team profiles returns the Windows one
profs, meta, err = ds.ListMDMConfigProfiles(ctx, &team.ID, opts)
require.NoError(t, err)
require.Len(t, profs, 1)
require.Equal(t, profB.Name, profs[0].Name)
require.Equal(t, *meta, fleet.PaginationMetadata{})
// create 12 labels for label-based profiles
var labels []*fleet.Label
for i := 0; i < 12; i++ {
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "l" + strconv.Itoa(i), Query: "select 1"})
require.NoError(t, err)
labels = append(labels, lbl)
}
// create more profiles and test the pagination with a table-driven test so that
// global and team both have 9 profiles (including A and B already created above).
for i := 0; i < 3; i++ {
inc := i * 6 // e.g. C, D, E, F, G, H on first loop, I, J, K, L, M, N on second loop, O, P, Q, R, S, T on third
// create label-based profiles for i==0, meaning CDEF will be label-based
acp := *generateAppleCP(string(rune('C'+inc)), string(rune('C'+inc)), 0) // C, I and O
if i == 0 {
acp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[0].Name, LabelID: labels[0].ID},
{LabelName: labels[1].Name, LabelID: labels[1].ID},
}
}
_, err = ds.NewMDMAppleConfigProfile(ctx, acp, nil)
require.NoError(t, err)
acp = *generateAppleCP(string(rune('C'+inc+1)), string(rune('C'+inc+1)), team.ID) // D, J and P
if i == 0 {
acp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[2].Name, LabelID: labels[2].ID},
{LabelName: labels[3].Name, LabelID: labels[3].ID},
}
}
_, err = ds.NewMDMAppleConfigProfile(ctx, acp, nil)
require.NoError(t, err)
wcp := fleet.MDMWindowsConfigProfile{ // E K and Q
Name: string(rune('C' + inc + 2)),
TeamID: nil,
SyncML: winProf,
}
if i == 0 {
wcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[4].Name, LabelID: labels[4].ID},
{LabelName: labels[5].Name, LabelID: labels[5].ID},
}
}
_, err = ds.NewMDMWindowsConfigProfile(ctx, wcp, nil)
require.NoError(t, err)
wcp = fleet.MDMWindowsConfigProfile{ // F L and R
Name: string(rune('C' + inc + 3)),
TeamID: &team.ID,
SyncML: winProf,
}
if i == 0 {
wcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[6].Name, LabelID: labels[6].ID},
{LabelName: labels[7].Name, LabelID: labels[7].ID},
}
}
_, err = ds.NewMDMWindowsConfigProfile(ctx, wcp, nil)
require.NoError(t, err)
// gcp = Android("google") config profile
gcp := fleet.MDMAndroidConfigProfile{ // G M and S
Name: string(rune('C' + inc + 4)),
TeamID: nil,
RawJSON: []byte(`{"hello4": "world4"}`),
}
if i == 0 {
gcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[8].Name, LabelID: labels[8].ID},
{LabelName: labels[9].Name, LabelID: labels[9].ID},
}
}
_, err = ds.NewMDMAndroidConfigProfile(ctx, gcp, nil)
require.NoError(t, err)
gcp = fleet.MDMAndroidConfigProfile{ // H N and T
Name: string(rune('C' + inc + 5)),
TeamID: &team.ID,
RawJSON: []byte(`{"hello5": "world5"}`),
}
if i == 0 {
gcp.LabelsIncludeAll = []fleet.ConfigurationProfileLabel{
{LabelName: labels[10].Name, LabelID: labels[10].ID},
{LabelName: labels[11].Name, LabelID: labels[11].ID},
}
}
_, err = ds.NewMDMAndroidConfigProfile(ctx, gcp, nil)
require.NoError(t, err)
}
// null label references to simulate profiles D, E and G being broken
// (FK is now RESTRICT so we can no longer delete a referenced label)
for _, lbl := range []*fleet.Label{labels[3], labels[4], labels[8]} {
simulateBrokenLabel(t, ds, ctx, lbl.Name)
}
profLabels := map[string][]fleet.ConfigurationProfileLabel{
"C": {
{LabelName: labels[0].Name, LabelID: labels[0].ID, RequireAll: true},
{LabelName: labels[1].Name, LabelID: labels[1].ID, RequireAll: true},
},
"D": {
{LabelName: labels[2].Name, LabelID: labels[2].ID, RequireAll: true},
{LabelName: labels[3].Name, LabelID: 0, Broken: true, RequireAll: true},
},
"E": {
{LabelName: labels[4].Name, LabelID: 0, Broken: true, RequireAll: true},
{LabelName: labels[5].Name, LabelID: labels[5].ID, RequireAll: true},
},
"F": {
{LabelName: labels[6].Name, LabelID: labels[6].ID, RequireAll: true},
{LabelName: labels[7].Name, LabelID: labels[7].ID, RequireAll: true},
},
"G": {
{LabelName: labels[8].Name, Broken: true, RequireAll: true},
{LabelName: labels[9].Name, LabelID: labels[9].ID, RequireAll: true},
},
"H": {
{LabelName: labels[10].Name, LabelID: labels[10].ID, RequireAll: true},
{LabelName: labels[11].Name, LabelID: labels[11].ID, RequireAll: true},
},
}
cases := []struct {
desc string
tmID *uint
opts fleet.ListOptions
wantNames []string
wantMeta fleet.PaginationMetadata
}{
{
"all global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true},
[]string{"A", "C", "E", "G", "I", "K", "M", "O", "Q", "S"},
fleet.PaginationMetadata{},
},
{
"all team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true},
[]string{"B", "D", "F", "H", "J", "L", "N", "P", "R", "T"},
fleet.PaginationMetadata{},
},
{
"page 0 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2},
[]string{"A", "C"},
fleet.PaginationMetadata{HasNextResults: true},
},
{
"page 1 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1},
[]string{"E", "G"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 2 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2},
[]string{"I", "K"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 3 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3},
[]string{"M", "O"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 4 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4},
[]string{"Q", "S"},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 5 per page 2, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 5},
[]string{},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 0 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2},
[]string{"B", "D"},
fleet.PaginationMetadata{HasNextResults: true},
},
{
"page 1 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 1},
[]string{"F", "H"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 2 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 2},
[]string{"J", "L"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 3 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 3},
[]string{"N", "P"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 4 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 4},
[]string{"R", "T"},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 5 per page 2, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 2, Page: 5},
[]string{},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 0 per page 3, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3},
[]string{"A", "C", "E"},
fleet.PaginationMetadata{HasNextResults: true},
},
{
"page 1 per page 3, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1},
[]string{"G", "I", "K"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 2 per page 3, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2},
[]string{"M", "O", "Q"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 3 per page 3, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3},
[]string{"S"},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 4 per page 3, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 4},
[]string{},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 0 per page 3, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3},
[]string{"B", "D", "F"},
fleet.PaginationMetadata{HasNextResults: true},
},
{
"page 1 per page 3, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 1},
[]string{"H", "J", "L"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 2 per page 3, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 2},
[]string{"N", "P", "R"},
fleet.PaginationMetadata{HasPreviousResults: true, HasNextResults: true},
},
{
"page 3 per page 3, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 3},
[]string{"T"},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"page 4 per page 3, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: true, PerPage: 3, Page: 4},
[]string{},
fleet.PaginationMetadata{HasPreviousResults: true},
},
{
"no metadata, global",
nil,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1},
[]string{"E", "G"},
fleet.PaginationMetadata{},
},
{
"no metadata, team",
&team.ID,
fleet.ListOptions{OrderKey: "name", IncludeMetadata: false, PerPage: 2, Page: 1},
[]string{"F", "H"},
fleet.PaginationMetadata{},
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
profs, meta, err := ds.ListMDMConfigProfiles(ctx, c.tmID, c.opts)
require.NoError(t, err)
require.Len(t, profs, len(c.wantNames))
got := make([]string, len(profs))
for i, p := range profs {
got[i] = p.Name
wantProfs := profLabels[p.Name]
require.Equal(t, len(wantProfs), len(p.LabelsIncludeAll), "profile name: %s", p.Name)
if len(wantProfs) > 0 {
// clear the profile uuids from the labels list
for i, l := range p.LabelsIncludeAll {
l.ProfileUUID = ""
p.LabelsIncludeAll[i] = l
}
require.ElementsMatch(t, wantProfs, p.LabelsIncludeAll, "profile name: %s", p.Name)
}
}
require.Equal(t, c.wantNames, got)
var gotMeta fleet.PaginationMetadata
if meta != nil {
gotMeta = *meta
}
require.Equal(t, c.wantMeta, gotMeta)
})
}
t.Run("rejects_unknown_order_key", func(t *testing.T) {
_, _, err := ds.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{OrderKey: "h.node_key"})
require.Error(t, err)
})
}
type anyProfile struct {
ProfileUUID string
Status *fleet.MDMDeliveryStatus
OperationType fleet.MDMOperationType
IdentifierOrName string
}
// cleanupStaleWindowsRemoveRows simulates the full Windows profile removal
// lifecycle (reconciler sends <Delete> → device confirms → row deleted) for
// remove rows that are NOT expected in the test assertions. Without this,
// remove rows from previous test phases accumulate and cause count mismatches.
//
// Scoped to only the Windows hosts present in the want map so that rows
// belonging to hosts not in the current assertion are left untouched. This
// prevents implicitly hiding issues for hosts the test phase doesn't check.
func cleanupStaleWindowsRemoveRows(t *testing.T, ds *Datastore, want map[*fleet.Host][]anyProfile) {
// Collect the set of Windows host UUIDs in the assertion and the
// (profile_uuid, host_uuid) pairs that are expected as remove rows.
wantWindowsHostUUIDs := make([]string, 0)
wantRemoveKeys := make(map[string]bool)
for h, profs := range want {
if h.Platform != "windows" {
continue
}
wantWindowsHostUUIDs = append(wantWindowsHostUUIDs, h.UUID)
for _, p := range profs {
if p.OperationType == fleet.MDMOperationTypeRemove {
wantRemoveKeys[p.ProfileUUID+"\n"+h.UUID] = true
}
}
}
if len(wantWindowsHostUUIDs) == 0 {
return
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
// Only select remove rows for hosts in the current assertion's want map.
stmt, args, err := sqlx.In(
`SELECT profile_uuid, host_uuid FROM host_mdm_windows_profiles WHERE operation_type = 'remove' AND host_uuid IN (?)`,
wantWindowsHostUUIDs)
if err != nil {
return err
}
var rows []struct {
ProfileUUID string `db:"profile_uuid"`
HostUUID string `db:"host_uuid"`
}
if err := sqlx.SelectContext(context.Background(), q, &rows, stmt, args...); err != nil {
return err
}
for _, r := range rows {
key := r.ProfileUUID + "\n" + r.HostUUID
if !wantRemoveKeys[key] {
if _, err := q.ExecContext(context.Background(),
`DELETE FROM host_mdm_windows_profiles WHERE profile_uuid = ? AND host_uuid = ? AND operation_type = 'remove'`,
r.ProfileUUID, r.HostUUID); err != nil {
return err
}
}
}
return nil
})
}
// assertHostProfiles only asserts the profile UUID, status, and operation.
func assertHostProfiles(t *testing.T, ds *Datastore, want map[*fleet.Host][]anyProfile) {
cleanupStaleWindowsRemoveRows(t, ds, want)
ctx := context.Background()
for h, wantProfs := range want {
var gotProfs []anyProfile
switch h.Platform {
case "windows":
profs, err := ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Equal(t, len(wantProfs), len(profs), "host uuid: %s", h.UUID)
for _, p := range profs {
gotProfs = append(gotProfs, anyProfile{
ProfileUUID: p.ProfileUUID,
Status: p.Status,
OperationType: p.OperationType,
IdentifierOrName: p.Name,
})
}
case "android":
profs, err := ds.GetHostMDMAndroidProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Len(t, profs, len(wantProfs), "host uuid: %s", h.UUID)
for _, p := range profs {
gotProfs = append(gotProfs, anyProfile{
ProfileUUID: p.ProfileUUID,
Status: p.Status,
OperationType: p.OperationType,
IdentifierOrName: p.Name,
})
}
default:
profs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Len(t, profs, len(wantProfs), "host uuid: %s", h.UUID)
for _, p := range profs {
gotProfs = append(gotProfs, anyProfile{
ProfileUUID: p.ProfileUUID,
Status: p.Status,
OperationType: p.OperationType,
IdentifierOrName: p.Identifier,
})
}
}
sortProfs := func(profs []anyProfile) []anyProfile {
sort.Slice(profs, func(i, j int) bool {
l, r := profs[i], profs[j]
if l.ProfileUUID == r.ProfileUUID {
return l.OperationType < r.OperationType
}
// default alphabetical comparison
return l.IdentifierOrName < r.IdentifierOrName
})
return profs
}
gotProfs = sortProfs(gotProfs)
wantProfs = sortProfs(wantProfs)
for i, wp := range wantProfs {
gp := gotProfs[i]
require.Equal(
t,
wp.ProfileUUID,
gp.ProfileUUID,
"host uuid: %s, prof id or name: %s",
h.UUID,
gp.IdentifierOrName,
)
require.Equal(
t,
wp.Status,
gp.Status,
"host uuid: %s, prof id or name: %s",
h.UUID,
gp.IdentifierOrName,
)
require.Equal(
t,
wp.OperationType,
gp.OperationType,
"host uuid: %s, prof id or name: %s",
h.UUID,
gp.IdentifierOrName,
)
}
}
}
func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore) {
ctx := context.Background()
// Setup funcs
// ===================================================
// MacOS base tests
// ===================================================
baseEarliestInstallDate := time.Now().UTC().Add(-24 * time.Hour).Truncate(time.Second)
overrideEarliestInstallDate := time.Now().UTC().Add(-6 * time.Hour).Truncate(time.Second)
macosBasicTeamProfNoLabelsSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-1",
OsqueryHostID: ptr.String("osquery-macos-1"),
NodeKey: ptr.String("node-key-macos-1"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// create profiles for team 1
profiles := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "T1.1", "T1.1", "d"),
configProfileForTest(t, "T1.2", "T1.2", "e"),
}
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 2)
return team.ID, host
}
macosLabeledTeamProfSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-2",
OsqueryHostID: ptr.String("osquery-macos-2"),
NodeKey: ptr.String("node-key-macos-2"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 2"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// create profiles for team 1
profiles := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "T2.1", "T2.1", "d"),
configProfileForTest(t, "T2.2", "T2.2", "e"),
configProfileForTest(t, "labeled_prof", "labeled_prof", "labeled_prof"),
}
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_1"})
require.NoError(t, err)
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(
ctx,
q,
&uid,
`SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
"labeled_prof",
)
})
// Update label with host membership
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
host.ID,
label.ID,
)
return err
},
)
// Update profile <-> label mapping
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)",
uid,
label.Name,
label.ID,
)
return err
},
)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 3)
return team.ID, host
}
macosLabeledTeamProfWithAdditionalLabeledProfSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-3",
OsqueryHostID: ptr.String("osquery-macos-3"),
NodeKey: ptr.String("node-key-macos-3"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 3"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// create profiles for team 1
profiles := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "T3.1", "T3.1", "d"),
configProfileForTest(t, "T3.2", "T3.2", "e"),
configProfileForTest(t, "labeled_prof_2", "labeled_prof_2", "labeled_prof_2"),
}
testLabel2, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_2"})
require.NoError(t, err)
testLabel3, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_3"})
require.NoError(t, err)
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(
ctx,
q,
&uid,
`SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
"labeled_prof_2",
)
})
// Update label with host membership
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
host.ID,
testLabel2.ID,
)
return err
},
)
// Update profile <-> label mapping
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)",
uid,
testLabel2.Name,
testLabel2.ID,
)
return err
},
)
// Also add mapping to test label 3
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)",
uid,
testLabel3.Name,
testLabel3.ID,
)
return err
},
)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 3)
return team.ID, host
}
macosProfWithBrokenLabelSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-4",
OsqueryHostID: ptr.String("osquery-macos-4"),
NodeKey: ptr.String("node-key-macos-4"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 4"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// create profiles for team
profiles := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "T4.1", "T4.1", "d"),
configProfileForTest(t, "T4.2", "T4.2", "e"),
configProfileForTest(t, "broken_label_prof", "broken_label_prof", "broken_label_prof"),
}
testLabel4, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_4"})
require.NoError(t, err)
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(
ctx,
q,
&uid,
`SELECT profile_uuid FROM mdm_apple_configuration_profiles WHERE identifier = ?`,
"broken_label_prof",
)
})
// Update label with host membership
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
host.ID,
testLabel4.ID,
)
return err
},
)
// Update profile <-> label mapping
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT INTO mdm_configuration_profile_labels (apple_profile_uuid, label_name, label_id) VALUES (?, ?, ?)",
uid,
testLabel4.Name,
testLabel4.ID,
)
return err
},
)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 3)
// Simulate the label being broken — direct DeleteLabel is now blocked when
// referenced by a profile, so we nullify label_id in the join tables instead.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
if _, err := q.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, testLabel4.ID); err != nil {
return err
}
_, err := q.ExecContext(ctx, `UPDATE mdm_declaration_labels SET label_id = NULL WHERE label_id = ?`, testLabel4.ID)
return err
})
return team.ID, host
}
macosInstallDateOverrideSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-5",
OsqueryHostID: ptr.String("osquery-macos-5"),
NodeKey: ptr.String("node-key-macos-5"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 5"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// create profiles for team 1
profiles := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "T5.1", "T5.1", "d"),
configProfileForTest(t, "T5.2", "T5.2", "e"),
configProfileForTest(t, "T5.3", "T5.3", "f"),
}
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 3)
// We cannot control the generated profile UUIDs here so we need to map them back to the
// created profiles for test correctness
for i := 0; i < len(profs); i++ {
for j := 0; j < len(profiles); j++ {
if profs[i].Identifier == profiles[j].Identifier {
profiles[j].ProfileUUID = profs[i].ProfileUUID
break
}
}
}
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"UPDATE mdm_apple_configuration_profiles SET uploaded_at = ? WHERE profile_uuid IN (?, ?, ?)",
baseEarliestInstallDate,
profiles[0].ProfileUUID,
profiles[1].ProfileUUID,
profiles[2].ProfileUUID,
)
return err
},
)
// Note what we're doing here is overriding install date for the first profile, creating an
// HMAP entry that doesn't override it for the second, then the third has no corresponding
// HMAP entry at all(which also means no override).
err = ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: profiles[0].ProfileUUID,
ProfileIdentifier: profiles[0].Identifier,
ProfileName: profiles[0].Name,
HostUUID: host.UUID,
CommandUUID: uuid.NewString(),
Checksum: profiles[0].Checksum,
VariablesUpdatedAt: &overrideEarliestInstallDate,
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
Scope: fleet.PayloadScopeSystem,
},
{
ProfileUUID: profiles[1].ProfileUUID,
ProfileIdentifier: profiles[1].Identifier,
ProfileName: profiles[1].Name,
HostUUID: host.UUID,
CommandUUID: uuid.NewString(),
Checksum: profiles[1].Checksum,
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
Scope: fleet.PayloadScopeSystem,
},
})
require.NoError(t, err)
return team.ID, host
}
macosLabeledProfileRulesSetup := func() (uint, *fleet.Host) {
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-6",
OsqueryHostID: ptr.String("osquery-macos-6"),
NodeKey: ptr.String("node-key-macos-6"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "macos team 6"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
require.NoError(t, err)
// Include any labels
includeAnyMatchedLabel1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-macos-matched-label-1"})
require.NoError(t, err)
includeAnyMatchedLabel2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-macos-matched-label-2"})
require.NoError(t, err)
includeAnyUnmatchedLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-macos-unmatched-label"})
require.NoError(t, err)
// Include all labels
includeAllMatchedLabel1, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-macos-matched-label-1"})
require.NoError(t, err)
includeAllMatchedLabel2, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-macos-matched-label-2"})
require.NoError(t, err)
includeAllUnmatchedLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-all-macos-unmatched-label"})
require.NoError(t, err)
// Exclude labels
excludeMatchedLabel1, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-macos-matched-label-1"})
require.NoError(t, err)
excludeMatchedLabel2, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-macos-matched-label-2"})
require.NoError(t, err)
excludeUnmatchedLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-macos-unmatched-label"})
require.NoError(t, err)
// create profiles for team
// include_any_all_match_prof
// include_any_one_matches_prof
// include_all_all_match_prof
// exclude_none_match_prof
profiles := []*fleet.MDMAppleConfigProfile{
// Two profiles with no label rules, thus should always be included
configProfileForTest(t, "T6.1", "T6.1", "g"),
configProfileForTest(t, "T6.2", "T6.2", "h"),
// This profile will use an "include any" rule where the host has both of the labels, thus should be included
configProfileForTest(t, "include_any_all_match_prof", "include_any_all_match_prof", "i", includeAnyMatchedLabel1, includeAnyMatchedLabel2),
// This profile will use an "include any" rule where the host has one of the labels, thus should be included
configProfileForTest(t, "include_any_one_matches_prof", "include_any_one_matches_prof", "j", includeAnyMatchedLabel1, includeAnyUnmatchedLabel),
// This profile will use an "include any" rule where the host has none of the labels, thus should be excluded
configProfileForTest(t, "include_any_none_match_prof", "include_any_none_match_prof", "k", includeAnyUnmatchedLabel),
// This profile will use an "include all" rule where the host has all of the labels and thus should be included
configProfileForTest(t, "include_all_all_match_prof", "include_all_all_match_prof", "l", includeAllMatchedLabel1, includeAllMatchedLabel2),
// This profile will use an "include all" rule where the host has one of the labels and thus should be excluded
configProfileForTest(t, "include_all_one_matches_prof", "include_all_one_matches_prof", "m", includeAllMatchedLabel1, includeAllUnmatchedLabel),
// This profile will use an "include any" rule where the host has none of the labels and thus should be excluded
configProfileForTest(t, "include_all_none_match_prof", "include_all_none_match_prof", "n", includeAllUnmatchedLabel),
// This profile will use an "exclude" rule where the host has both of the labels, thus should be excluded
configProfileForTest(t, "exclude_all_match_prof", "exclude_all_match_prof", "o", excludeMatchedLabel1, excludeMatchedLabel2),
// This profile will use an "exclude" rule where the host has one of the labels, thus should be excluded
configProfileForTest(t, "exclude_one_matches_prof", "exclude_one_matches_prof", "p", excludeMatchedLabel1, excludeUnmatchedLabel),
// This profile will use an "exclude" rule where the host has none of the labels, thus should be included
configProfileForTest(t, "exclude_none_match_prof", "exclude_none_match_prof", "q", excludeUnmatchedLabel),
// This profile will use both an "include all" and "exclude" rule where the host matches the include all rule but also matches the exclude rule, thus should be excluded
configProfileForTest(t, "include_all_and_exclude_match_prof", "include_all_and_exclude_match_prof", "r", includeAllMatchedLabel1, includeAllMatchedLabel2, excludeMatchedLabel1),
// This profile will use both an "include all" and "exclude" rule where the host matches the include all rule but does not match the exclude rule, thus should be included
configProfileForTest(t, "include_all_and_exclude_none_match_prof", "include_all_and_exclude_none_match_prof", "s", includeAllMatchedLabel1, includeAllMatchedLabel2, excludeUnmatchedLabel),
// This profile will use both an "include any" and "exclude" rule where the host matches the include any rule but also matches the exclude rule, thus should be excluded
configProfileForTest(t, "include_any_and_exclude_match_prof", "include_any_and_exclude_match_prof", "t", includeAnyMatchedLabel1, includeAnyUnmatchedLabel, excludeMatchedLabel1),
// This profile will use both an "include any" and "exclude" rule where the host matches the include any rule but does not match the exclude rule, thus should be included
configProfileForTest(t, "include_any_and_exclude_none_match_prof", "include_any_and_exclude_none_match_prof", "u", includeAnyMatchedLabel1, includeAnyUnmatchedLabel, excludeUnmatchedLabel),
}
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
// Update labels with host membership
ExecAdhocSQL(
t, ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)",
host.ID,
includeAnyMatchedLabel1.ID,
host.ID,
includeAnyMatchedLabel2.ID,
host.ID,
includeAllMatchedLabel1.ID,
host.ID,
includeAllMatchedLabel2.ID,
host.ID,
excludeMatchedLabel1.ID,
host.ID,
excludeMatchedLabel2.ID,
)
return err
},
)
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 15)
return team.ID, host
}
tests := []struct {
name string
setupFunc func() (uint, *fleet.Host)
wantMac map[string]*fleet.ExpectedMDMProfile
os string
}{
{
name: "macos basic team profiles no labels",
setupFunc: macosBasicTeamProfNoLabelsSetup,
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T1.1": {Identifier: "T1.1"},
"T1.2": {Identifier: "T1.2"},
},
},
{
name: "macos labeled team profile",
setupFunc: macosLabeledTeamProfSetup,
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T2.1": {Identifier: "T2.1"},
"T2.2": {Identifier: "T2.2"},
"labeled_prof": {Identifier: "labeled_prof"},
},
},
{
name: "macos labeled team profile with additional labeled profile",
setupFunc: macosLabeledTeamProfWithAdditionalLabeledProfSetup,
// Our expected profiles should not include the labeled profile, because it
// maps to a label that is not applied to the host.
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T3.1": {Identifier: "T3.1"},
"T3.2": {Identifier: "T3.2"},
},
},
{
name: "macos profile with broken label",
setupFunc: macosProfWithBrokenLabelSetup,
// Our expected profiles should not include the labeled profile, because it is broken
// (the label was deleted)
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T4.1": {Identifier: "T4.1"},
"T4.2": {Identifier: "T4.2"},
},
},
{
name: "macos basic team profiles no labels, install date overridden on first",
setupFunc: macosInstallDateOverrideSetup,
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T5.1": {Identifier: "T5.1", EarliestInstallDate: overrideEarliestInstallDate},
"T5.2": {Identifier: "T5.2", EarliestInstallDate: baseEarliestInstallDate},
"T5.3": {Identifier: "T5.3", EarliestInstallDate: baseEarliestInstallDate},
},
},
{
name: "macos labels include any/all and exclude rules",
setupFunc: macosLabeledProfileRulesSetup,
wantMac: map[string]*fleet.ExpectedMDMProfile{
"T6.1": {Identifier: "T6.1"},
"T6.2": {Identifier: "T6.2"},
"include_any_all_match_prof": {Identifier: "include_any_all_match_prof"},
"include_any_one_matches_prof": {Identifier: "include_any_one_matches_prof"},
"include_all_all_match_prof": {Identifier: "include_all_all_match_prof"},
"exclude_none_match_prof": {Identifier: "exclude_none_match_prof"},
"include_all_and_exclude_none_match_prof": {Identifier: "include_all_and_exclude_none_match_prof"},
"include_any_and_exclude_none_match_prof": {Identifier: "include_any_and_exclude_none_match_prof"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
teamID, host := tt.setupFunc()
timeZero := time.Time{}
if len(tt.wantMac) > 0 {
got, err := ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID, host)
require.NoError(t, err)
for k, v := range tt.wantMac {
require.Contains(t, got, k)
require.Equal(t, v.Identifier, got[k].Identifier)
// Only check earliest install date if we are overriding it in the test setup
if v.EarliestInstallDate != timeZero {
require.Equal(t, v.EarliestInstallDate, got[k].EarliestInstallDate)
}
}
}
})
}
}
func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a label
label := &fleet.Label{
Name: "my label",
Description: "a label",
Query: "select 1 from processes;",
}
label, err := ds.NewLabel(ctx, label)
require.NoError(t, err)
// create a macOS config profile
macOSProfile, err := ds.NewMDMAppleConfigProfile(
ctx,
fleet.MDMAppleConfigProfile{
Name: "DummyTestName",
Identifier: "DummyTestIdentifier",
Mobileconfig: mobileconfig.Mobileconfig([]byte("DummyTestMobileconfigBytes")),
TeamID: nil,
},
nil,
)
require.NoError(t, err)
otherMacProfile, err := ds.NewMDMAppleConfigProfile(
ctx,
fleet.MDMAppleConfigProfile{
Name: "OtherDummyTestName",
Identifier: "OtherDummyTestIdentifier",
Mobileconfig: mobileconfig.Mobileconfig([]byte("OtherDummyTestMobileconfigBytes")),
TeamID: nil,
},
nil,
)
require.NoError(t, err)
// create a Windows config profile
windowsProfile, err := ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{
Name: "with-labels",
TeamID: nil,
SyncML: []byte("<Replace></Replace>"),
},
nil,
)
require.NoError(t, err)
otherWinProfile, err := ds.NewMDMWindowsConfigProfile(
ctx,
fleet.MDMWindowsConfigProfile{
Name: "other-with-labels",
TeamID: nil,
SyncML: []byte("<Replace></Replace>"),
},
nil,
)
require.NoError(t, err)
// assign the label to the "other" profiles, should not change throughout the test
wantOtherWin := []fleet.ConfigurationProfileLabel{
{ProfileUUID: otherWinProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID},
}
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, []string{windowsProfile.ProfileUUID}, "windows")
require.NoError(t, err)
assert.True(t, updatedDB)
// make it an "exclude" label on the other macos profile
wantOtherMac := []fleet.ConfigurationProfileLabel{
{ProfileUUID: otherMacProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID, Exclude: true},
}
updatedDB, err = batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherMac, []string{macOSProfile.ProfileUUID}, "darwin")
require.NoError(t, err)
assert.True(t, updatedDB)
platforms := map[string]string{
"darwin": macOSProfile.ProfileUUID,
"windows": windowsProfile.ProfileUUID,
}
for platform, uuid := range platforms {
expectLabels := func(t *testing.T, profUUID, platform string, want []fleet.ConfigurationProfileLabel) {
p := platform
if p == "darwin" {
p = "apple"
}
query := fmt.Sprintf(
"SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ?",
p,
p,
)
var got []fleet.ConfigurationProfileLabel
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
err := sqlx.SelectContext(ctx, tx, &got, query, profUUID)
require.NoError(t, err)
require.Len(t, got, len(want))
return nil
})
require.ElementsMatch(t, want, got)
}
t.Run("empty input "+platform, func(t *testing.T) {
want := []fleet.ConfigurationProfileLabel{}
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, want, nil, platform)
require.NoError(t, err)
assert.False(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, want)
// does not change other profiles
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
})
t.Run("valid input "+platform, func(t *testing.T) {
profileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
}
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
require.NoError(t, err)
assert.True(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, profileLabels)
// does not change other profiles
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
// now set it with Exclude mode
profileLabels = []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID, Exclude: true},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
require.NoError(t, err)
assert.True(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, profileLabels)
// does not change other profiles
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
})
t.Run("invalid profile UUID "+platform, func(t *testing.T) {
invalidProfileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: "invalid-uuid", LabelName: label.Name, LabelID: label.ID},
}
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, nil, platform)
return err
})
require.Error(t, err)
})
t.Run("invalid label data "+platform, func(t *testing.T) {
// invalid id
invalidProfileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: label.Name, LabelID: 12345},
}
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, nil, platform)
return err
})
require.Error(t, err)
// both invalid
invalidProfileLabels = []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: "xyz", LabelID: 1235},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, nil, platform)
return err
})
require.Error(t, err)
})
t.Run("labels are removed "+platform, func(t *testing.T) {
// create a new label
newLabel := &fleet.Label{
Name: "new label" + platform,
Description: "a label",
Query: "select 1 from orbit_info;",
}
newLabel, err := ds.NewLabel(ctx, newLabel)
require.NoError(t, err)
// apply a batch set with the new label
profileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID, Exclude: true},
{ProfileUUID: uuid, LabelName: newLabel.Name, LabelID: newLabel.ID, Exclude: true},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
require.NoError(t, err)
assert.True(t, updatedDB)
return err
})
require.NoError(t, err)
// both are stored in the DB
expectLabels(t, uuid, platform, profileLabels)
// batch apply again without the newLabel, and without Exclude flag
profileLabels = []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
require.NoError(t, err)
assert.True(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, profileLabels)
// batch apply again this time without any label
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, nil, []string{uuid}, platform)
require.NoError(t, err)
assert.True(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, nil)
// does not change other profiles
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
// batch apply again with no change returns false
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, nil, []string{uuid}, platform)
require.NoError(t, err)
assert.False(t, updatedDB)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, nil)
})
t.Run("broken label association is cleared on upsert "+platform, func(t *testing.T) {
// Regression test for https://github.com/fleetdm/fleet/issues/42637.
startingLabel := &fleet.Label{
Name: "broken-label-" + platform,
Query: "select 1 from osquery_info;",
}
startingLabel, err := ds.NewLabel(ctx, startingLabel)
require.NoError(t, err)
profileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: startingLabel.Name, LabelID: startingLabel.ID, Exclude: true},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
return err
})
require.NoError(t, err)
expectLabels(t, uuid, platform, profileLabels)
// Simulate the label being deleted: NULL the label_id directly. This is
// what FK ON DELETE SET NULL does when the underlying label row is gone.
p := platform
if p == "darwin" {
p = "apple"
}
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx,
fmt.Sprintf(`UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE %s_profile_uuid = ? AND label_name = ?`, p),
uuid, startingLabel.Name)
return err
})
// Sanity: broken row exists.
var brokenCount int
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
return sqlx.GetContext(ctx, tx, &brokenCount,
fmt.Sprintf(`SELECT COUNT(*) FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ? AND label_id IS NULL`, p),
uuid)
})
require.Equal(t, 1, brokenCount, "expected broken row to exist before re-upsert")
// Re-apply with a different label, mimicking gitops switching from
// labels_exclude_any: [startingLabel] to labels_include_any: [switchTarget].
switchTarget := &fleet.Label{
Name: "switch-target-" + platform,
Query: "select 1 from osquery_info;",
}
switchTarget, err = ds.NewLabel(ctx, switchTarget)
require.NoError(t, err)
profileLabels = []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: switchTarget.Name, LabelID: switchTarget.ID, Exclude: false},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
return err
})
require.NoError(t, err)
// The broken row must be gone
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
return sqlx.GetContext(ctx, tx, &brokenCount,
fmt.Sprintf(`SELECT COUNT(*) FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ? AND label_id IS NULL`, p),
uuid)
})
require.Equal(t, 0, brokenCount, "broken (NULL label_id) row should have been cleared")
// Only switchTarget should remain.
expectLabels(t, uuid, platform, profileLabels)
// Other profiles must remain untouched.
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
})
t.Run("same label name recreated after deletion "+platform, func(t *testing.T) {
// Regression test for https://github.com/fleetdm/fleet/issues/44950.
// Reproduces: global label referenced by a profile is deleted (label_id
// becomes NULL via ON DELETE SET NULL), then a fleet-level label with
// the SAME name is created and re-applied. The SELECT in
// batchSetProfileLabelAssociationsDB used to fail with
// "converting NULL to uint is unsupported" because it selected the
// broken row's NULL label_id into a uint field.
origLabel := &fleet.Label{
Name: "gitops-label-" + platform,
Query: "select 1 from osquery_info;",
}
origLabel, err := ds.NewLabel(ctx, origLabel)
require.NoError(t, err)
// Associate profile with original label
profileLabels := []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: origLabel.Name, LabelID: origLabel.ID},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
return err
})
require.NoError(t, err)
// Simulate the label being deleted: nullify label_id in the join table
// first (RESTRICT FK now blocks direct delete), then remove the label row.
// The test then re-creates a label with the same name to verify the
// broken-link upsert path.
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
if _, err := tx.ExecContext(ctx, `UPDATE mdm_configuration_profile_labels SET label_id = NULL WHERE label_id = ?`, origLabel.ID); err != nil {
return err
}
_, err := tx.ExecContext(ctx, `DELETE FROM labels WHERE id = ?`, origLabel.ID)
return err
})
p := platform
if p == "darwin" {
p = "apple"
}
// Sanity check: the broken (NULL) row exists.
var brokenBefore int
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
return sqlx.GetContext(ctx, tx, &brokenBefore,
fmt.Sprintf(`SELECT COUNT(*) FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ? AND label_id IS NULL`, p),
uuid)
})
require.Equal(t, 1, brokenBefore, "expected broken row after label deletion")
// Create a new label with the SAME name (simulates moving from global to fleet scope)
newLabel := &fleet.Label{
Name: origLabel.Name,
Query: "select 1 from osquery_info;",
}
newLabel, err = ds.NewLabel(ctx, newLabel)
require.NoError(t, err)
// Re-apply with the new label ID but same name — this is the
// call that used to fail with "converting NULL to uint".
profileLabels = []fleet.ConfigurationProfileLabel{
{ProfileUUID: uuid, LabelName: newLabel.Name, LabelID: newLabel.ID},
}
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, nil, platform)
return err
})
require.NoError(t, err, "batchSetProfileLabelAssociationsDB should not fail when a broken row exists with the same label name")
// Verify only the new label association exists (no broken row)
var brokenAfter int
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
return sqlx.GetContext(ctx, tx, &brokenAfter,
fmt.Sprintf(`SELECT COUNT(*) FROM mdm_configuration_profile_labels WHERE %s_profile_uuid = ? AND label_id IS NULL`, p),
uuid)
})
require.Equal(t, 0, brokenAfter, "broken (NULL label_id) row should have been cleaned up")
// Verify the correct label association exists with the new label ID.
expectLabels(t, uuid, platform, profileLabels)
// Other profiles must remain untouched.
expectLabels(t, otherWinProfile.ProfileUUID, "windows", wantOtherWin)
expectLabels(t, otherMacProfile.ProfileUUID, "darwin", wantOtherMac)
})
}
t.Run("unsupported platform", func(t *testing.T) {
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
_, err := batchSetProfileLabelAssociationsDB(
ctx,
tx,
[]fleet.ConfigurationProfileLabel{{}},
nil,
"unsupported",
)
return err
})
require.Error(t, err)
})
}
func testMDMEULA(t *testing.T, ds *Datastore) {
ctx := context.Background()
eula := &fleet.MDMEULA{
Token: uuid.New().String(),
Name: "eula.pdf",
Bytes: []byte("contents"),
Sha256: []byte("test-sha256"),
}
err := ds.MDMInsertEULA(ctx, eula)
require.NoError(t, err)
var ae fleet.AlreadyExistsError
err = ds.MDMInsertEULA(ctx, eula)
require.ErrorAs(t, err, &ae)
gotEULA, err := ds.MDMGetEULAMetadata(ctx)
require.NoError(t, err)
require.NotEmpty(t, gotEULA.CreatedAt)
require.Equal(t, eula.Token, gotEULA.Token)
require.Equal(t, eula.Name, gotEULA.Name)
gotEULABytes, err := ds.MDMGetEULABytes(ctx, eula.Token)
require.NoError(t, err)
require.EqualValues(t, eula.Bytes, gotEULABytes.Bytes)
require.Equal(t, eula.Name, gotEULABytes.Name)
err = ds.MDMDeleteEULA(ctx, eula.Token)
require.NoError(t, err)
var nfe fleet.NotFoundError
_, err = ds.MDMGetEULAMetadata(ctx)
require.ErrorAs(t, err, &nfe)
_, err = ds.MDMGetEULABytes(ctx, eula.Token)
require.ErrorAs(t, err, &nfe)
err = ds.MDMDeleteEULA(ctx, eula.Token)
require.ErrorAs(t, err, &nfe)
err = ds.MDMInsertEULA(ctx, eula)
require.NoError(t, err)
}
func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
ctx := context.Background()
scepDepot, err := ds.NewSCEPDepot()
require.NoError(t, err)
nanoStorage, err := ds.NewMDMAppleMDMStorage()
require.NoError(t, err)
addCert := func(notAfter time.Time, h *fleet.Host) {
serial, err := scepDepot.Serial()
require.NoError(t, err)
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "Fleet Identity",
},
NotBefore: time.Now().Add(-24 * time.Hour),
NotAfter: notAfter,
// use a random value, just to make sure they're
// different from each other, we don't care about the
// DER contents here
Raw: []byte(uuid.NewString()),
}
err = scepDepot.Put(cert.Subject.CommonName, cert)
require.NoError(t, err)
req := mdm.Request{
EnrollID: &mdm.EnrollID{ID: h.UUID},
Context: ctx,
}
certHash := certauth.HashCert(cert)
err = nanoStorage.AssociateCertHash(&req, certHash, notAfter)
require.NoError(t, err)
createdat := time.Now().AddDate(0, int(serial.Int64()), 0)
// due to mysql timestamp resolution, this test is flaky unless
// we do this because we insert multiple certificates for the
// same device in quick succession, and later on we assert on a
// specific order which is based on created_at
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE nano_cert_auth_associations
SET created_at = ?
WHERE sha256 = ?
`, createdat, certHash)
return err
})
}
var i int
setHost := func(notAfter time.Time, doUserDeviceEnrollment bool) *fleet.Host {
i++
h, err := ds.NewHost(ctx, &fleet.Host{
Hostname: fmt.Sprintf("test-host%d-name", i),
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
UUID: fmt.Sprintf("test-uuid-%d", i),
Platform: "darwin",
})
require.NoError(t, err)
// create a cert + association
addCert(notAfter, h)
if doUserDeviceEnrollment {
nanoEnrollUserDevice(t, ds, h)
} else {
nanoEnroll(t, ds, h, true)
}
return h
}
// certs expired at lest 1 year ago
h1 := setHost(time.Now().AddDate(-1, -3, 0), false)
h2 := setHost(time.Now().AddDate(-1, -2, 0), false)
// cert that expires in 1 month
h3 := setHost(time.Now().AddDate(0, 1, 0), false)
// User Enrollment (Device) cert that expires in 1 month and 1 day just
// so we can add some assertions on the returned enrollment type
h4 := setHost(time.Now().AddDate(0, 1, 1), true)
// cert that expires in 1 year
h5 := setHost(time.Now().AddDate(1, 0, 0), false)
// expired cert for a host migrated using touchless migration
hMigrated := setHost(time.Now().AddDate(-1, -1, 0), false)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
UPDATE nano_enrollments
SET enrolled_from_migration = 1
WHERE id = ?
`, hMigrated.UUID)
return err
})
// list assocs that expire in the next 10 days
assocs, err := ds.GetHostCertAssociationsToExpire(ctx, 10, 100)
require.NoError(t, err)
require.Len(t, assocs, 3)
require.Equal(t, h1.UUID, assocs[0].HostUUID)
require.Equal(t, h2.UUID, assocs[1].HostUUID)
require.Equal(t, hMigrated.UUID, assocs[2].HostUUID)
require.True(t, assocs[2].EnrolledFromMigration)
// list certs that expire in the next 1000 days with limit = 1
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 1)
require.NoError(t, err)
require.Len(t, assocs, 1)
require.Equal(t, h1.UUID, assocs[0].HostUUID)
// list certs that expire in the next 50 days
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 50, 100)
require.NoError(t, err)
require.Len(t, assocs, 5)
require.Equal(t, h1.UUID, assocs[0].HostUUID)
assert.Equal(t, "Device", assocs[0].EnrollmentType)
require.Equal(t, h2.UUID, assocs[1].HostUUID)
assert.Equal(t, "Device", assocs[1].EnrollmentType)
require.Equal(t, hMigrated.UUID, assocs[2].HostUUID)
assert.Equal(t, "Device", assocs[2].EnrollmentType)
require.Equal(t, h3.UUID, assocs[3].HostUUID)
assert.Equal(t, "Device", assocs[3].EnrollmentType)
require.Equal(t, h4.UUID, assocs[4].HostUUID)
assert.Equal(t, "User Enrollment (Device)", assocs[4].EnrollmentType)
// list certs that expire in the next 1000 days
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 100)
require.NoError(t, err)
require.Len(t, assocs, 6)
require.Equal(t, h1.UUID, assocs[0].HostUUID)
assert.Equal(t, "Device", assocs[0].EnrollmentType)
require.Equal(t, h2.UUID, assocs[1].HostUUID)
assert.Equal(t, "Device", assocs[1].EnrollmentType)
require.Equal(t, hMigrated.UUID, assocs[2].HostUUID)
assert.Equal(t, "Device", assocs[2].EnrollmentType)
require.Equal(t, h3.UUID, assocs[3].HostUUID)
assert.Equal(t, "Device", assocs[3].EnrollmentType)
require.Equal(t, h4.UUID, assocs[4].HostUUID)
assert.Equal(t, "User Enrollment (Device)", assocs[4].EnrollmentType)
require.Equal(t, h5.UUID, assocs[5].HostUUID)
assert.Equal(t, "Device", assocs[5].EnrollmentType)
// add a new host with a very old expiriy so it shows first, verify
// that it's present before deleting it.
h6 := setHost(time.Now().AddDate(-2, -1, 0), false)
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 100)
require.NoError(t, err)
require.Len(t, assocs, 7)
require.Equal(t, h6.UUID, assocs[0].HostUUID)
require.Equal(t, h1.UUID, assocs[1].HostUUID)
require.Equal(t, h2.UUID, assocs[2].HostUUID)
require.Equal(t, hMigrated.UUID, assocs[3].HostUUID)
require.Equal(t, h3.UUID, assocs[4].HostUUID)
require.Equal(t, h4.UUID, assocs[5].HostUUID)
require.Equal(t, h5.UUID, assocs[6].HostUUID)
// delete the host and verify that things work as expected
// see https://github.com/fleetdm/fleet/issues/19149
require.NoError(t, ds.DeleteHost(ctx, h6.ID))
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 100)
require.NoError(t, err)
require.Len(t, assocs, 6)
require.Equal(t, h1.UUID, assocs[0].HostUUID)
require.Equal(t, h2.UUID, assocs[1].HostUUID)
require.Equal(t, hMigrated.UUID, assocs[2].HostUUID)
require.Equal(t, h3.UUID, assocs[3].HostUUID)
require.Equal(t, h4.UUID, assocs[4].HostUUID)
require.Equal(t, h5.UUID, assocs[5].HostUUID)
// add a second expired cert to one of the hosts
addCert(time.Now().AddDate(-1, 0, 0), h1)
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 100)
require.Len(t, assocs, 6)
require.Equal(t, h2.UUID, assocs[0].HostUUID)
require.Equal(t, hMigrated.UUID, assocs[1].HostUUID)
require.Equal(t, h1.UUID, assocs[2].HostUUID)
require.Equal(t, h3.UUID, assocs[3].HostUUID)
require.Equal(t, h4.UUID, assocs[4].HostUUID)
require.Equal(t, h5.UUID, assocs[5].HostUUID)
checkSCEPRenew := func(assoc fleet.SCEPIdentityAssociation, want *string) {
var got *string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(
ctx,
q,
&got,
`SELECT renew_command_uuid FROM nano_cert_auth_associations WHERE id = ? AND sha256 = ?`,
assoc.HostUUID,
assoc.SHA256,
)
})
require.EqualValues(t, want, got)
}
// insert dummy nano commands
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `
INSERT INTO nano_commands (command_uuid, request_type, command)
VALUES ('foo', 'foo', '<?xml'), ('bar', 'bar', '<?xml')
`)
return err
})
err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{}, "foo")
checkSCEPRenew(assocs[0], nil)
checkSCEPRenew(assocs[1], nil)
checkSCEPRenew(assocs[2], nil)
checkSCEPRenew(assocs[3], nil)
checkSCEPRenew(assocs[4], nil)
checkSCEPRenew(assocs[5], nil)
require.NoError(t, err)
err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{assocs[0]}, "foo")
require.NoError(t, err)
checkSCEPRenew(assocs[0], ptr.String("foo"))
checkSCEPRenew(assocs[1], nil)
checkSCEPRenew(assocs[2], nil)
checkSCEPRenew(assocs[3], nil)
checkSCEPRenew(assocs[4], nil)
checkSCEPRenew(assocs[5], nil)
err = ds.SetCommandForPendingSCEPRenewal(ctx, assocs, "bar")
require.NoError(t, err)
checkSCEPRenew(assocs[0], ptr.String("bar"))
checkSCEPRenew(assocs[1], ptr.String("bar"))
checkSCEPRenew(assocs[2], ptr.String("bar"))
checkSCEPRenew(assocs[3], ptr.String("bar"))
checkSCEPRenew(assocs[4], ptr.String("bar"))
checkSCEPRenew(assocs[5], ptr.String("bar"))
err = ds.SetCommandForPendingSCEPRenewal(
ctx,
[]fleet.SCEPIdentityAssociation{{HostUUID: "foo", SHA256: "bar"}},
"bar",
)
require.ErrorContains(t, err, "this function can only be used to update existing associations")
err = ds.CleanSCEPRenewRefs(ctx, "does-not-exist")
require.Error(t, err)
err = ds.CleanSCEPRenewRefs(ctx, h1.UUID)
require.NoError(t, err)
checkSCEPRenew(assocs[2], nil)
}
func testMDMProfilesSummaryAndHostFilters(t *testing.T, ds *Datastore) {
// TODO: Expand this test to include:
// - more scenarios for windows
// - disk encryption (mac and windows)
// - more scenarios for labels
ctx := context.Background()
checkSummaryWindows := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) {
// GetMDMWindowsProfilesSummary reads the maintained host_mdm_windows_profiles_status rollup; this test seeds
// host_mdm_windows_profiles directly, so reconcile it first.
require.NoError(t, ds.ReconcileWindowsProfilesStatus(ctx))
ps, err := ds.GetMDMWindowsProfilesSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, expected, *ps)
}
checkSummaryMac := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) {
ps, err := ds.GetMDMAppleProfilesSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, expected, *ps)
}
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
gotHosts, err := ds.ListHosts(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status},
)
require.NoError(t, err)
if len(expectedIDs) != len(gotHosts) {
gotIDs := make([]uint, len(gotHosts))
for _, h := range gotHosts {
gotIDs = append(gotIDs, h.ID)
}
require.Len(
t,
gotHosts,
len(expectedIDs),
fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs),
)
}
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
count, err := ds.CountHosts(
ctx,
fleet.TeamFilter{User: test.UserAdmin},
fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status},
)
require.NoError(t, err)
require.Equal(t, len(expectedIDs), count, "status: %s", status)
}
type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint
checkExpected := func(t *testing.T, teamID *uint, ep hostIDsByProfileStatus) {
expectSummaryWindows := map[fleet.MDMDeliveryStatus]uint{}
expectSummaryMac := map[fleet.MDMDeliveryStatus]uint{}
for status, ids := range ep {
if len(ids) > 0 {
for _, id := range ids {
if id < 5 {
expectSummaryWindows[status]++
} else {
expectSummaryMac[status]++
}
}
}
}
checkSummaryMac(t, teamID, fleet.MDMProfilesSummary{
Pending: expectSummaryMac[fleet.MDMDeliveryPending],
Failed: expectSummaryMac[fleet.MDMDeliveryFailed],
Verifying: expectSummaryMac[fleet.MDMDeliveryVerifying],
Verified: expectSummaryMac[fleet.MDMDeliveryVerified],
})
checkSummaryWindows(t, teamID, fleet.MDMProfilesSummary{
Pending: expectSummaryWindows[fleet.MDMDeliveryPending],
Failed: expectSummaryWindows[fleet.MDMDeliveryFailed],
Verifying: expectSummaryWindows[fleet.MDMDeliveryVerifying],
Verified: expectSummaryWindows[fleet.MDMDeliveryVerified],
})
checkListHostsFilterOSSettings(
t,
teamID,
fleet.OSSettingsVerified,
ep[fleet.MDMDeliveryVerified],
)
checkListHostsFilterOSSettings(
t,
teamID,
fleet.OSSettingsVerifying,
ep[fleet.MDMDeliveryVerifying],
)
checkListHostsFilterOSSettings(
t,
teamID,
fleet.OSSettingsFailed,
ep[fleet.MDMDeliveryFailed],
)
checkListHostsFilterOSSettings(
t,
teamID,
fleet.OSSettingsPending,
ep[fleet.MDMDeliveryPending],
)
}
// checkWinHostProfiles := func(t *testing.T, hostUUID string, statusByProfUUID map[string]string) {
// profs, err := ds.GetHostMDMWindowsProfiles(ctx, hostUUID)
// require.NoError(t, err)
// require.Len(t, profs, len(statusByProfUUID))
// for _, prof := range profs {
// ep, ok := statusByProfUUID[prof.ProfileUUID]
// require.True(t, ok)
// require.Equal(t, ep, prof.Status)
// }
// }
checkMacHostProfiles := func(t *testing.T, hostUUID string, statusByProfUUID map[string]string) {
profs, err := ds.GetHostMDMAppleProfiles(ctx, hostUUID)
require.NoError(t, err)
require.Len(t, profs, len(statusByProfUUID))
for _, prof := range profs {
ep, ok := statusByProfUUID[prof.ProfileUUID]
require.True(t, ok)
require.NotNil(t, prof.Status)
require.Equal(t, fleet.MDMDeliveryStatus(ep), *prof.Status)
}
}
upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var table string
var profType string
switch {
case strings.HasPrefix(profUUID, "a"):
table = "host_mdm_apple_profiles"
profType = "profile"
case strings.HasPrefix(profUUID, "w"):
table = "host_mdm_windows_profiles"
profType = "profile"
case strings.HasPrefix(profUUID, "d"):
table = "host_mdm_apple_declarations"
profType = "declaration"
default:
require.FailNow(t, "unknown profile type")
}
var stmt string
var err error
switch {
case table == "host_mdm_apple_profiles":
// Apple profiles require profile_identifier, checksum, and command_uuid
commandUUID := "cmd-" + profUUID
stmt = fmt.Sprintf(
`INSERT INTO %s (host_uuid, %s_uuid, status, profile_identifier, checksum, command_uuid) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`,
table,
profType,
)
testChecksum := test.MakeTestBytes() // 16 bytes for checksum
_, err = q.ExecContext(ctx, stmt, hostUUID, profUUID, status, "com.test."+profUUID, testChecksum, commandUUID, status)
case table == "host_mdm_apple_declarations":
// Apple declarations require declaration_identifier and token
testToken := test.MakeTestBytes() // 16 bytes for token
stmt = fmt.Sprintf(
`INSERT INTO %s (host_uuid, %s_uuid, status, declaration_identifier, token) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`,
table,
profType,
)
_, err = q.ExecContext(ctx, stmt, hostUUID, profUUID, status, "com.test."+profUUID, testToken, status)
case strings.HasPrefix(profUUID, "w"):
// Windows profiles require command_uuid
commandUUID := "cmd-" + profUUID
stmt = fmt.Sprintf(
`INSERT INTO %s (host_uuid, %s_uuid, status, command_uuid) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`,
table,
profType,
)
_, err = q.ExecContext(ctx, stmt, hostUUID, profUUID, status, commandUUID, status)
default:
stmt = fmt.Sprintf(
`INSERT INTO %s (host_uuid, %s_uuid, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`,
table,
profType,
)
_, err = q.ExecContext(ctx, stmt, hostUUID, profUUID, status, status)
}
if err != nil {
require.NoError(t, err)
return err
}
stmt = fmt.Sprintf(
`UPDATE %s SET operation_type = ? WHERE host_uuid = ? AND %s_uuid = ?`,
table,
profType,
)
_, err = q.ExecContext(ctx, stmt, fleet.MDMOperationTypeInstall, hostUUID, profUUID)
require.NoError(t, err)
return err
})
}
cleanupTables := func(t *testing.T) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_windows_profiles`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_declarations`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_disk_encryption_keys`)
return err
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_disks`)
return err
})
}
// updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) {
// ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
// stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?`
// _, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID)
// return err
// })
// }
// Create some hosts
var hosts []*fleet.Host
macHostsByID := make(map[uint]*fleet.Host, 5)
winHostsByID := make(map[uint]*fleet.Host, 5)
for i := 0; i < 10; i++ {
p := "windows"
if i >= 5 {
p = "darwin"
}
u := uuid.New().String()
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: &u,
UUID: u,
Hostname: u,
Platform: p,
})
require.NoError(t, err)
require.NotNil(t, h)
hosts = append(hosts, h)
if p == "darwin" {
nanoEnroll(t, ds, h, false)
macHostsByID[h.ID] = h
} else {
winHostsByID[h.ID] = h
windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: h.UUID,
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
require.NoError(t, err)
}
require.NoError(
t,
ds.SetOrUpdateMDMData(
ctx,
h.ID,
false,
true,
"https://example.com",
false,
fleet.WellKnownMDMFleet,
"",
false,
),
)
}
checkExpected(t, nil, nil)
upsertHostProfileStatus(t, hosts[0].UUID, "w1", &fleet.MDMDeliveryPending)
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
})
// add some mac profiles with different statuses
upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryFailed)
upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryPending)
upsertHostProfileStatus(t, hosts[9].UUID, "a3", &fleet.MDMDeliveryVerifying)
upsertHostProfileStatus(t, hosts[9].UUID, "a4", &fleet.MDMDeliveryVerified)
// add some mac declarations with different statuses
upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryFailed)
upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryPending)
upsertHostProfileStatus(t, hosts[9].UUID, "d3", &fleet.MDMDeliveryVerifying)
upsertHostProfileStatus(t, hosts[9].UUID, "d4", &fleet.MDMDeliveryVerified)
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryFailed: []uint{hosts[9].ID},
})
expectedHostProfiles := map[string]string{
"a1": "failed",
"a2": "pending",
"a3": "verifying",
"a4": "verified",
"d1": "failed",
"d2": "pending",
"d3": "verifying",
"d4": "verified",
}
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set failed mac profile to pending, still failed because of failed declaration
upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryPending)
expectedHostProfiles["a1"] = "pending"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryFailed: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set failed mac declaration to pending, now host stsatus is pending
upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryPending)
expectedHostProfiles["d1"] = "pending"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set pending mac declaration to failed, host status is now failed
upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryFailed)
expectedHostProfiles["d2"] = "failed"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryFailed: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set failed mac declaration to verifying, host status is now pending
upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryVerifying)
expectedHostProfiles["d2"] = "verifying"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set pending mac profiles to verifying, host status is still pending because d1 is still pending
upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryVerifying)
expectedHostProfiles["a1"] = "verifying"
upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryVerifying)
expectedHostProfiles["a2"] = "verifying"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set pending mac declarations to verifying, host status is now verifying
upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryVerifying)
expectedHostProfiles["d1"] = "verifying"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set a mac profile to failed, host status is now failed
upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryFailed)
expectedHostProfiles["a1"] = "failed"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryFailed: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set mac profiles to verified, host status is now verifying because declarations are still
// verifying
upsertHostProfileStatus(t, hosts[9].UUID, "a1", &fleet.MDMDeliveryVerified)
expectedHostProfiles["a1"] = "verified"
upsertHostProfileStatus(t, hosts[9].UUID, "a2", &fleet.MDMDeliveryVerified)
expectedHostProfiles["a2"] = "verified"
upsertHostProfileStatus(t, hosts[9].UUID, "a3", &fleet.MDMDeliveryVerified)
expectedHostProfiles["a3"] = "verified"
upsertHostProfileStatus(t, hosts[9].UUID, "a4", &fleet.MDMDeliveryVerified)
expectedHostProfiles["a4"] = "verified"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set mac declarations to verified, host status is now verified
upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryVerified)
expectedHostProfiles["d1"] = "verified"
upsertHostProfileStatus(t, hosts[9].UUID, "d2", &fleet.MDMDeliveryVerified)
expectedHostProfiles["d2"] = "verified"
upsertHostProfileStatus(t, hosts[9].UUID, "d3", &fleet.MDMDeliveryVerified)
expectedHostProfiles["d3"] = "verified"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryVerified: []uint{hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// set a mac declaration to nil, host status is now pending
upsertHostProfileStatus(t, hosts[9].UUID, "d1", nil)
expectedHostProfiles["d1"] = "pending"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// works as expected if we remove mac declarations
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_declarations`)
return err
})
delete(expectedHostProfiles, "d1")
delete(expectedHostProfiles, "d2")
delete(expectedHostProfiles, "d3")
delete(expectedHostProfiles, "d4")
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
fleet.MDMDeliveryVerified: []uint{hosts[9].ID}, // all profiles were verified
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// works as expected if we remove mac profiles
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_apple_profiles`)
return err
})
delete(expectedHostProfiles, "a1")
delete(expectedHostProfiles, "a2")
delete(expectedHostProfiles, "a3")
delete(expectedHostProfiles, "a4")
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
// works as expected if declarations but no profiles
upsertHostProfileStatus(t, hosts[9].UUID, "d1", &fleet.MDMDeliveryPending)
expectedHostProfiles["d1"] = "pending"
checkExpected(t, nil, hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[9].ID},
})
checkMacHostProfiles(t, hosts[9].UUID, expectedHostProfiles)
cleanupTables(t)
}
func testAreHostsConnectedToFleetMDM(t *testing.T, ds *Datastore) {
ctx := context.Background()
notConnectedMac, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test",
OsqueryHostID: ptr.String("osquery-macos-not-connected"),
NodeKey: ptr.String("node-key-macos-not-connected"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
connectedMac, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test",
OsqueryHostID: ptr.String("osquery-macos"),
NodeKey: ptr.String("node-key-macos"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, connectedMac, false)
err = ds.SetOrUpdateMDMData(ctx, connectedMac.ID, false, true, "http://foo.com", false, "foo", "", false)
require.NoError(t, err)
disconnectedWithoutCheckoutMac, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test-disconnected",
OsqueryHostID: ptr.String("osquery-macos-disconnected"),
NodeKey: ptr.String("node-key-macos-disconnected"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
nanoEnroll(t, ds, disconnectedWithoutCheckoutMac, false)
err = ds.SetOrUpdateMDMData(ctx, disconnectedWithoutCheckoutMac.ID, false, false, "", false, "", "", false)
require.NoError(t, err)
notConnectedWin, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "windows-test",
OsqueryHostID: ptr.String("osquery-windows-not-connected"),
NodeKey: ptr.String("node-key-windows-not-connected"),
UUID: uuid.NewString(),
Platform: "windows",
})
require.NoError(t, err)
connectedWin, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "windows-test",
OsqueryHostID: ptr.String("osquery-windows"),
NodeKey: ptr.String("node-key-windows"),
UUID: uuid.NewString(),
Platform: "windows",
})
require.NoError(t, err)
windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: connectedWin.UUID,
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(ctx, connectedWin.ID, false, true, "http://foo.com", false, "foo", "", false)
require.NoError(t, err)
disconnectedWithoutCheckoutWin, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "windows-test-disconnected",
OsqueryHostID: ptr.String("osquery-windows-disconnected"),
NodeKey: ptr.String("node-key-windows-disconnected"),
UUID: uuid.NewString(),
Platform: "windows",
})
require.NoError(t, err)
windowsEnrollmentDisconnectedWithoutCheckout := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1-disconnected",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: disconnectedWithoutCheckoutWin.UUID,
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollmentDisconnectedWithoutCheckout)
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(ctx, disconnectedWithoutCheckoutWin.ID, false, false, "", false, "", "", false)
require.NoError(t, err)
connectedMap, err := ds.AreHostsConnectedToFleetMDM(ctx, []*fleet.Host{
notConnectedMac,
connectedMac,
connectedWin,
notConnectedWin,
disconnectedWithoutCheckoutMac,
disconnectedWithoutCheckoutWin,
})
require.NoError(t, err)
require.Equal(t, map[string]bool{
notConnectedMac.UUID: false,
connectedMac.UUID: true,
connectedWin.UUID: true,
notConnectedWin.UUID: false,
disconnectedWithoutCheckoutMac.UUID: false,
disconnectedWithoutCheckoutWin.UUID: false,
}, connectedMap)
linuxHost, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "linux-test",
OsqueryHostID: ptr.String("osquery-linux"),
NodeKey: ptr.String("node-key-linux"),
UUID: uuid.NewString(),
Platform: "linux",
})
require.NoError(t, err)
connectedMap, err = ds.AreHostsConnectedToFleetMDM(ctx, []*fleet.Host{
notConnectedMac,
connectedMac,
connectedWin,
notConnectedWin,
linuxHost,
disconnectedWithoutCheckoutMac,
disconnectedWithoutCheckoutWin,
})
require.NoError(t, err)
require.Equal(t, map[string]bool{
notConnectedMac.UUID: false,
connectedMac.UUID: true,
connectedWin.UUID: true,
notConnectedWin.UUID: false,
linuxHost.UUID: false,
disconnectedWithoutCheckoutMac.UUID: false,
disconnectedWithoutCheckoutWin.UUID: false,
}, connectedMap)
}
func testIsHostConnectedToFleetMDM(t *testing.T, ds *Datastore) {
ctx := context.Background()
// requireConnected asserts that IsHostConnectedToFleetMDM and the connected_to_fleet flag computed by GetHostMDM agree with the
// expected value. GetOrbitConfig derives the connection state from GetHostMDM instead of a separate IsHostConnectedToFleetMDM
// query, so the two must stay in lockstep across every enrollment state. When the host has no host_mdm row, GetHostMDM returns
// NotFound and the host cannot be connected.
requireConnected := func(t *testing.T, h *fleet.Host, want bool) {
t.Helper()
connected, err := ds.IsHostConnectedToFleetMDM(ctx, h)
require.NoError(t, err)
require.Equal(t, want, connected)
mdmInfo, err := ds.GetHostMDM(ctx, h.ID)
if err != nil {
require.True(t, fleet.IsNotFound(err))
require.False(t, want, "host without a host_mdm row cannot be connected to Fleet MDM")
return
}
require.Equal(t, want, mdmInfo.ConnectedToFleet)
}
macH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test",
OsqueryHostID: ptr.String("osquery-macos"),
NodeKey: ptr.String("node-key-macos"),
UUID: uuid.NewString(),
Platform: "darwin",
})
require.NoError(t, err)
requireConnected(t, macH, false)
nanoEnroll(t, ds, macH, false)
err = ds.SetOrUpdateMDMData(ctx, macH.ID, false, true, "http://foo.com", false, "foo", "", false)
require.NoError(t, err)
requireConnected(t, macH, true)
byodIpadH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "ipados-test",
OsqueryHostID: ptr.String("osquery-ipados"),
NodeKey: ptr.String("node-key-ipados"),
UUID: uuid.NewString(),
Platform: "ipados",
})
require.NoError(t, err)
nanoEnrollUserDevice(t, ds, byodIpadH)
err = ds.SetOrUpdateMDMData(ctx, byodIpadH.ID, false, true, "http://foo.com", false, "foo", "", false)
require.NoError(t, err)
requireConnected(t, byodIpadH, true)
windowsH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "windows-test",
OsqueryHostID: ptr.String("osquery-windows"),
NodeKey: ptr.String("node-key-windows"),
UUID: uuid.NewString(),
Platform: "windows",
})
require.NoError(t, err)
requireConnected(t, windowsH, false)
windowsEnrollment := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: uuid.New().String(),
MDMHardwareID: uuid.New().String() + uuid.New().String(),
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: "CIMClient_Windows",
MDMDeviceName: "DESKTOP-1C3ARC1",
MDMEnrollType: "ProgrammaticEnrollment",
MDMEnrollUserID: "",
MDMEnrollProtoVersion: "5.0",
MDMEnrollClientVersion: "10.0.19045.2965",
MDMNotInOOBE: false,
HostUUID: windowsH.UUID,
}
err = ds.MDMWindowsInsertEnrolledDevice(ctx, windowsEnrollment)
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(ctx, windowsH.ID, false, true, "http://foo.com", false, "foo", "", false)
require.NoError(t, err)
requireConnected(t, windowsH, true)
// now simulate an un-enrollment without checkout, in this case, osquery reports the host as not-enrolled
err = ds.SetOrUpdateMDMData(ctx, macH.ID, false, false, "", false, "", "", false)
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(ctx, windowsH.ID, false, false, "", false, "", "", false)
require.NoError(t, err)
requireConnected(t, macH, false)
requireConnected(t, windowsH, false)
// Simulate the ipad checking out(user removing work account)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE nano_enrollments SET enabled = 0 WHERE id = ?`, byodIpadH.UUID)
return err
})
requireConnected(t, byodIpadH, false)
// Android: connection is determined solely by host_mdm.enrolled, so the connected_to_fleet column must track enrollment without
// any separate enrollment record.
androidH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "android-test",
OsqueryHostID: new("osquery-android"),
NodeKey: new("node-key-android"),
UUID: uuid.NewString(),
Platform: "android",
})
require.NoError(t, err)
requireConnected(t, androidH, false)
err = ds.SetOrUpdateMDMData(ctx, androidH.ID, false, true, "http://foo.com", false, fleet.WellKnownMDMFleet, "", false)
require.NoError(t, err)
requireConnected(t, androidH, true)
err = ds.SetOrUpdateMDMData(ctx, androidH.ID, false, false, "", false, "", "", false)
require.NoError(t, err)
requireConnected(t, androidH, false)
}
// This test now only covers android, as the other platforms no longer rely on the BulkSetPendingMDMHostProfiles,
// but rather the reconciler to work out the truth after some runs.
func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := context.Background()
// create some "exclude" labels
var labels []*fleet.Label
for i := 0; i < 8; i++ {
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-label-" + strconv.Itoa(i), Query: "select 1"})
require.NoError(t, err)
labels = append(labels, lbl)
}
androidProfs := []*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "G1", nil, labels[6]),
}
updates, err := ds.BatchSetMDMProfiles(ctx, nil, nil, nil, nil, androidProfs, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile)
// must reload them to get the profile/declaration uuid
getProfs := func(teamID *uint) []*fleet.MDMConfigProfilePayload {
// TODO(roberto): the docs says that you can pass a comma separated
// list of columns to OrderKey, but that doesn't seem to work
profs, _, err := ds.ListMDMConfigProfiles(ctx, teamID, fleet.ListOptions{})
require.NoError(t, err)
sort.Slice(profs, func(i, j int) bool {
l, r := profs[i], profs[j]
if l.Platform != r.Platform {
return l.Platform < r.Platform
}
return l.Name < r.Name
})
return profs
}
allProfs := getProfs(nil)
var i int
androidHostObj := createAndroidHost(fmt.Sprintf("android-host%d-name", i))
androidHostObj, err = ds.NewAndroidHost(ctx, androidHostObj, false)
androidHost := androidHostObj.Host
require.NoError(t, err)
// Set LabelUpdatedAt to a time before labels were created to simulate hosts that haven't reported label membership
// Use a time in the past (2020) to ensure it's before any label created in this test
oldTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
androidHost.LabelUpdatedAt = oldTime
err = ds.UpdateHost(ctx, androidHost)
require.NoError(t, err)
// at this point the hosts have not reported any label results, so a sync
// does NOT install the exclude any profiles as we don't know yet if the
// hosts will be members or not
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID}, nil, nil, nil)
require.NoError(t, err)
assert.False(t, updates.AppleConfigProfile)
assert.False(t, updates.WindowsConfigProfile)
assert.False(t, updates.AppleDeclaration)
assert.False(t, updates.AndroidConfigProfile)
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {},
})
// setting the LabelsUpdatedAt timestamp means that labels results were reported, so now
// the profiles will be installed as the hosts are not members of the excluded labels.
androidHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, androidHost)
require.NoError(t, err)
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID}, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile)
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {
{
ProfileUUID: allProfs[0].ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
IdentifierOrName: allProfs[0].Name,
},
},
})
// make all hosts members of labels[1], [2], [3], [6] so that all profiles are
// excluded
err = ds.AsyncBatchInsertLabelMembership(ctx, [][2]uint{
{labels[1].ID, androidHost.ID},
{labels[2].ID, androidHost.ID},
{labels[3].ID, androidHost.ID},
{labels[6].ID, androidHost.ID},
})
require.NoError(t, err)
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID}, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile)
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {
{
ProfileUUID: allProfs[0].ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeRemove,
IdentifierOrName: allProfs[0].Name,
},
},
})
// make apple host member of labels[2], and windows and android host member of [3], which are irrelevant
// for their platforms' profiles, so they get all profiles
err = ds.AsyncBatchDeleteLabelMembership(ctx, [][2]uint{
{labels[1].ID, androidHost.ID},
{labels[2].ID, androidHost.ID},
{labels[6].ID, androidHost.ID},
})
require.NoError(t, err)
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID}, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile)
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {
{
ProfileUUID: allProfs[0].ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
IdentifierOrName: allProfs[0].Name,
},
},
})
// null label references to break all profiles (FK is now RESTRICT so we can no longer delete referenced labels)
for _, lbl := range []*fleet.Label{labels[0], labels[2], labels[3], labels[6]} {
simulateBrokenLabel(t, ds, ctx, lbl.Name)
}
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID}, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile) // "Broken profiles" behave differently for android.
// broken profiles do not get reported as "to remove", except for android
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {
{
ProfileUUID: allProfs[0].ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeRemove,
IdentifierOrName: allProfs[0].Name,
},
},
})
i++
androidHostObj2 := createAndroidHost(fmt.Sprintf("android-host%d-name", i))
androidHostObj2, err = ds.NewAndroidHost(ctx, androidHostObj2, false)
require.NoError(t, err)
androidHost2 := androidHostObj2.Host
androidHost2.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, androidHost2)
require.NoError(t, err)
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{androidHost.ID, androidHost2.ID}, nil, nil, nil)
require.NoError(t, err)
assert.True(t, updates.AndroidConfigProfile) // True from first host
// broken profiles do not get reported as "to install"
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
androidHost: {
{
ProfileUUID: allProfs[0].ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeRemove,
IdentifierOrName: allProfs[0].Name,
},
},
androidHost2: {},
})
}
// testBulkSetPendingDefersWindowsReconciliation verifies the production
// behavior of BulkSetPendingMDMHostProfiles: it does not synchronously
// write host_mdm_windows_profiles (the mdm_windows_profile_manager cron
// handles that on its next 30s tick), and updates.WindowsConfigProfile
// stays false here. The accurate "Windows profile changed" signal comes
// from batchSetMDMWindowsProfilesDB inside BatchSetMDMProfiles, which is
// the only consumer.
func testBulkSetPendingDefersWindowsReconciliation(t *testing.T, ds *Datastore) {
ctx := t.Context()
// This test verifies the production async path. The eager hook is
// off by default, so no opt-in is needed here.
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "defer-windows-recon-test"})
require.NoError(t, err)
windowsProfs := []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "DW1", "DW1"),
}
_, err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, windowsProfs, nil, nil, nil)
require.NoError(t, err)
host := test.NewHost(t, ds, "dw-host", "dw1", "dw1key", "dw-host-uuid", time.Now(),
test.WithPlatform("windows"), test.WithTeamID(team.ID))
windowsEnroll(t, ds, host)
// Sanity: host_mdm_windows_profiles starts empty for this host.
before, err := ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
require.NoError(t, err)
require.Empty(t, before)
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, []uint{host.ID}, nil, nil, nil)
require.NoError(t, err)
// Production path leaves WindowsConfigProfile false: the lone consumer
// of this field (service/mdm.go's BatchSetMDMProfiles flow) ORs it with
// profUpdates.WindowsConfigProfile from BatchSetMDMProfiles, which is
// the accurate transactional signal. With the eager hook disabled here
// to match production, this method does not compute the activity
// signal itself.
assert.False(t, updates.WindowsConfigProfile,
"production path leaves WindowsConfigProfile false; activity is logged by BatchSetMDMProfiles")
// host_mdm_windows_profiles must not have been written synchronously.
// In production the cron will write it on the next tick; in this test
// nothing else runs and we go straight to the assertion.
after, err := ds.GetHostMDMWindowsProfiles(ctx, host.UUID)
require.NoError(t, err)
assert.Empty(t, after,
"BulkSetPendingMDMHostProfiles must not write host_mdm_windows_profiles synchronously")
// The batched reconciler walks all enrolled Windows hosts each pass, so no
// per-host pending signal is needed for the deferred work to be picked up.
}
func testBatchResendProfileToHosts(t *testing.T, ds *Datastore) {
ctx := t.Context()
// create some hosts and some profiles
host1 := test.NewHost(t, ds, "host1", "1", "h1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "h2key", "host2uuid", time.Now())
host3 := test.NewHost(t, ds, "host3", "3", "h3key", "host3uuid", time.Now())
host4 := test.NewHost(t, ds, "host4", "4", "h4key", "host4uuid", time.Now())
// create a team and make host4 part of that team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host4.ID}))
require.NoError(t, err)
// create some profiles , a and b for no team, c for team
profA, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil)
require.NoError(t, err)
profB, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 0), nil)
require.NoError(t, err)
profC, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("c", "c", team.ID), nil)
require.NoError(t, err)
t.Logf("profA=%s, profB=%s, profC=%s", profA.ProfileUUID, profB.ProfileUUID, profC.ProfileUUID)
assertHostProfileStatus(t, ds, host1.UUID)
assertHostProfileStatus(t, ds, host2.UUID)
assertHostProfileStatus(t, ds, host3.UUID)
assertHostProfileStatus(t, ds, host4.UUID)
// make profile A installed for all no team
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
// batch-resend profile A, does not impact any host
n, err := ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile B, does not impact any host as it's not delievered to any yet
n, err = ds.BatchResendMDMProfileToHosts(ctx, profB.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerifying})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile A failed on a couple hosts, verified on the other
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
// batch-resend profile A, impacts host 1 and 2
n, err = ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 2, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile A again, no change as it is already pending on the impacted hosts
n, err = ds.BatchResendMDMProfileToHosts(ctx, profA.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile B failed on all hosts
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryFailed})
assertHostProfileStatus(t, ds, host4.UUID)
// batch-resend profile B, all hosts affected
n, err = ds.BatchResendMDMProfileToHosts(ctx, profB.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 3, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryPending},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryPending})
assertHostProfileStatus(t, ds, host4.UUID)
// make profile C failed on host 4, other profiles Verified
forceSetAppleHostProfileStatus(t, ds, host4.UUID, profC, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profA, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profB, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
// batch-resend profile C, host 4 affected
n, err = ds.BatchResendMDMProfileToHosts(ctx, profC.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 1, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID,
hostProfileStatus{profC.ProfileUUID, fleet.MDMDeliveryPending})
// batch-resend profile C again, no change
n, err = ds.BatchResendMDMProfileToHosts(ctx, profC.ProfileUUID, fleet.BatchResendMDMProfileFilters{ProfileStatus: fleet.MDMDeliveryFailed})
require.NoError(t, err)
require.EqualValues(t, 0, n)
assertHostProfileStatus(t, ds, host1.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host2.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host3.UUID,
hostProfileStatus{profA.ProfileUUID, fleet.MDMDeliveryVerified},
hostProfileStatus{profB.ProfileUUID, fleet.MDMDeliveryVerified})
assertHostProfileStatus(t, ds, host4.UUID,
hostProfileStatus{profC.ProfileUUID, fleet.MDMDeliveryPending})
}
func testGetMDMConfigProfileStatus(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := t.Context()
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
// create some Apple and Windows profiles and declaration for No team
appleProfsNoTm := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "A1", "A1", uuid.NewString()),
}
windowsProfsNoTm := []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W1", "W1"),
}
appleDeclsNoTm := []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "{}"),
}
// Create an android profile for No Team
androidProfilesNoTm := []*fleet.MDMAndroidConfigProfile{
androidProfileForTest("G1"),
}
_, err = ds.BatchSetMDMProfiles(ctx, nil, appleProfsNoTm, windowsProfsNoTm, appleDeclsNoTm, androidProfilesNoTm, nil)
require.NoError(t, err)
// create some Apple and Windows profiles and declaration for the team
appleProfsTm := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "A2", "A2", uuid.NewString()),
}
windowsProfsTm := []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W2", "W2"),
}
appleDeclsTm := []*fleet.MDMAppleDeclaration{
declForTest("D2", "D2", "{}"),
}
androidProfilesTm := []*fleet.MDMAndroidConfigProfile{
androidProfileForTest("G2"),
}
androidProfilesTm[0].TeamID = &team.ID
_, err = ds.BatchSetMDMProfiles(ctx, &team.ID, appleProfsTm, windowsProfsTm, appleDeclsTm, androidProfilesTm, nil)
require.NoError(t, err)
// collect the profiles in a lookup table by name
profNameToProf := make(map[string]*fleet.MDMConfigProfilePayload)
profs, _, err := ds.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{})
require.NoError(t, err)
for _, prof := range profs {
profNameToProf[prof.Name] = prof
}
profs, _, err = ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
for _, prof := range profs {
profNameToProf[prof.Name] = prof
}
// create some hosts, macOS and Windows, No team and Team
host1 := test.NewHost(t, ds, "host1", "1", "h1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "h2key", "host2uuid", time.Now())
host3 := test.NewHost(t, ds, "host3", "3", "h3key", "host3uuid", time.Now())
host4 := test.NewHost(t, ds, "host4", "4", "h4key", "host4uuid", time.Now())
host5 := test.NewHost(t, ds, "host5", "5", "h5key", "host5uuid", time.Now())
nanoEnroll(t, ds, host1, false)
nanoEnroll(t, ds, host2, false)
nanoEnroll(t, ds, host3, false)
nanoEnroll(t, ds, host4, false)
nanoEnroll(t, ds, host5, false)
host6 := test.NewHost(t, ds, "host6", "6", "h6key", "host6uuid", time.Now())
host6.Platform = "windows"
err = ds.UpdateHost(ctx, host6)
require.NoError(t, err)
windowsEnroll(t, ds, host6)
host7 := test.NewHost(t, ds, "host7", "7", "h7key", "host7uuid", time.Now())
host7.Platform = "windows"
err = ds.UpdateHost(ctx, host7)
require.NoError(t, err)
windowsEnroll(t, ds, host7)
host8 := test.NewHost(t, ds, "host8", "8", "h8key", "host8uuid", time.Now())
host8.Platform = "windows"
err = ds.UpdateHost(ctx, host8)
require.NoError(t, err)
windowsEnroll(t, ds, host8)
androidHost9 := createAndroidHost("enterprise-id-9")
newHost, err := ds.NewAndroidHost(context.Background(), androidHost9, false)
require.NoError(t, err)
require.NotNil(t, newHost)
host9 := newHost.Host
androidHost10 := createAndroidHost("enterprise-id-10")
newHost, err = ds.NewAndroidHost(context.Background(), androidHost10, false)
require.NoError(t, err)
require.NotNil(t, newHost)
host10 := newHost.Host
androidHost11 := createAndroidHost("enterprise-id-11")
newHost, err = ds.NewAndroidHost(context.Background(), androidHost11, false)
require.NoError(t, err)
require.NotNil(t, newHost)
host11 := newHost.Host
for _, h := range []*fleet.Host{host1, host2, host3, host4, host5, host6, host7, host8} {
err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://fleetdm.com", false, fleet.WellKnownMDMFleet, "", false)
require.NoError(t, err)
}
// host 4, 5, 8, 10, 11 are on team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host4.ID, host5.ID, host8.ID, host10.ID, host11.ID}))
require.NoError(t, err)
_, _, _, _ = host1, host2, host3, host11
// currently no status for any profile
for _, name := range []string{"A1", "W1", "D1", "A2", "W2", "D2", "G1", "G2"} {
status, err := ds.GetMDMConfigProfileStatus(ctx, profNameToProf[name].ProfileUUID)
require.NoError(t, err, name)
require.Equal(t, fleet.MDMConfigProfileStatus{}, status, name)
}
// unknown profile with invalid prefix is not found (we don't test unknown
// profile with valid prefix here as the not found is handled at the service
// layer - it loads the profile for authorization)
_, err = ds.GetMDMConfigProfileStatus(ctx, "ZZ-NOT-FOUND")
require.Error(t, err)
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
cases := []struct {
desc string
profileUUID string
setup func(t *testing.T)
want fleet.MDMConfigProfileStatus
}{
{
desc: "macOS no team profile A1 all pending/NULL",
profileUUID: profNameToProf["A1"].ProfileUUID,
setup: func(t *testing.T) {
appleA1 := test.ToMDMAppleConfigProfile(profNameToProf["A1"])
forceSetAppleHostProfileStatus(t, ds, host1.UUID, appleA1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetAppleHostProfileStatus(t, ds, host2.UUID, appleA1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetAppleHostProfileStatus(t, ds, host3.UUID, appleA1, fleet.MDMOperationTypeInstall, "")
},
want: fleet.MDMConfigProfileStatus{Pending: 3},
},
{
desc: "windows no team profile W1 all pending/NULL",
profileUUID: profNameToProf["W1"].ProfileUUID,
setup: func(t *testing.T) {
winW1 := test.ToMDMWindowsConfigProfile(profNameToProf["W1"])
forceSetWindowsHostProfileStatus(t, ds, host6.UUID, winW1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetWindowsHostProfileStatus(t, ds, host7.UUID, winW1, fleet.MDMOperationTypeInstall, "")
},
want: fleet.MDMConfigProfileStatus{Pending: 2},
},
{
desc: "windows no team profile W1 pending failed",
profileUUID: profNameToProf["W1"].ProfileUUID,
setup: func(t *testing.T) {
winW1 := test.ToMDMWindowsConfigProfile(profNameToProf["W1"])
forceSetWindowsHostProfileStatus(t, ds, host6.UUID, winW1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetWindowsHostProfileStatus(t, ds, host7.UUID, winW1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
},
want: fleet.MDMConfigProfileStatus{Pending: 1, Failed: 1},
},
{
desc: "macOS no team decl D1 pending failed verified",
profileUUID: profNameToProf["D1"].ProfileUUID,
setup: func(t *testing.T) {
declD1 := test.ToMDMAppleDecl(profNameToProf["D1"])
forceSetAppleHostDeclarationStatus(t, ds, host1.UUID, declD1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetAppleHostDeclarationStatus(t, ds, host2.UUID, declD1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostDeclarationStatus(t, ds, host3.UUID, declD1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
},
want: fleet.MDMConfigProfileStatus{Pending: 1, Failed: 1, Verified: 1},
},
{
desc: "macOS team profile A2 verifying verified",
profileUUID: profNameToProf["A2"].ProfileUUID,
setup: func(t *testing.T) {
appleA2 := test.ToMDMAppleConfigProfile(profNameToProf["A2"])
forceSetAppleHostProfileStatus(t, ds, host4.UUID, appleA2, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
forceSetAppleHostProfileStatus(t, ds, host5.UUID, appleA2, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
},
want: fleet.MDMConfigProfileStatus{Verifying: 1, Verified: 1},
},
{
desc: "macOS team decl D2 all failed",
profileUUID: profNameToProf["D2"].ProfileUUID,
setup: func(t *testing.T) {
declD2 := test.ToMDMAppleDecl(profNameToProf["D2"])
forceSetAppleHostDeclarationStatus(t, ds, host4.UUID, declD2, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
forceSetAppleHostDeclarationStatus(t, ds, host5.UUID, declD2, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryFailed)
},
want: fleet.MDMConfigProfileStatus{Failed: 2},
},
{
desc: "windows team profile W2 pending",
profileUUID: profNameToProf["W2"].ProfileUUID,
setup: func(t *testing.T) {
winW2 := test.ToMDMWindowsConfigProfile(profNameToProf["W2"])
forceSetWindowsHostProfileStatus(t, ds, host8.UUID, winW2, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
},
want: fleet.MDMConfigProfileStatus{Pending: 1},
},
{
desc: "android no team G1 profile failed",
profileUUID: profNameToProf["G1"].ProfileUUID,
setup: func(t *testing.T) {
androidG1 := profNameToProf["G1"]
upsertAndroidHostProfileStatus(t, ds, host9.UUID, androidG1.ProfileUUID, &fleet.MDMDeliveryFailed)
},
want: fleet.MDMConfigProfileStatus{Failed: 1},
},
{
desc: "android team G2 profile nil=pending",
profileUUID: profNameToProf["G2"].ProfileUUID,
setup: func(t *testing.T) {
androidG2 := profNameToProf["G2"]
upsertAndroidHostProfileStatus(t, ds, host10.UUID, androidG2.ProfileUUID, nil)
},
want: fleet.MDMConfigProfileStatus{Pending: 1},
},
{
desc: "android team G2 profile nil + pending = 2 pending",
profileUUID: profNameToProf["G2"].ProfileUUID,
setup: func(t *testing.T) {
androidG2 := profNameToProf["G2"]
upsertAndroidHostProfileStatus(t, ds, host10.UUID, androidG2.ProfileUUID, nil)
upsertAndroidHostProfileStatus(t, ds, host11.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryPending)
},
want: fleet.MDMConfigProfileStatus{Pending: 2},
},
{
desc: "android team G2 profile pending + verifying",
profileUUID: profNameToProf["G2"].ProfileUUID,
setup: func(t *testing.T) {
androidG2 := profNameToProf["G2"]
upsertAndroidHostProfileStatus(t, ds, host10.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryVerifying)
upsertAndroidHostProfileStatus(t, ds, host11.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryPending)
},
want: fleet.MDMConfigProfileStatus{Pending: 1, Verifying: 1},
},
{
desc: "android team G2 profile verified + verifying",
profileUUID: profNameToProf["G2"].ProfileUUID,
setup: func(t *testing.T) {
androidG2 := profNameToProf["G2"]
upsertAndroidHostProfileStatus(t, ds, host10.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryVerified)
upsertAndroidHostProfileStatus(t, ds, host11.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryVerifying)
},
want: fleet.MDMConfigProfileStatus{Verifying: 1, Verified: 1},
},
{
desc: "android team G2 profile failed + verified",
profileUUID: profNameToProf["G2"].ProfileUUID,
setup: func(t *testing.T) {
// Set an unrelated profile status on another team just to ensure isolation
androidG1 := profNameToProf["G1"]
upsertAndroidHostProfileStatus(t, ds, host9.UUID, androidG1.ProfileUUID, &fleet.MDMDeliveryPending)
androidG2 := profNameToProf["G2"]
upsertAndroidHostProfileStatus(t, ds, host10.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryVerified)
upsertAndroidHostProfileStatus(t, ds, host11.UUID, androidG2.ProfileUUID, &fleet.MDMDeliveryFailed)
},
want: fleet.MDMConfigProfileStatus{Failed: 1, Verified: 1},
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
c.setup(t)
got, err := ds.GetMDMConfigProfileStatus(ctx, c.profileUUID)
require.NoError(t, err)
require.Equal(t, c.want, got)
})
}
}
// enableWindowsMDMForReconcileTest enables Windows MDM in app config so ReconcileWindowsProfiles does work in tests, restoring the
// previous value afterward (it is a global flag that would otherwise leak into sibling subtests sharing the datastore).
func enableWindowsMDMForReconcileTest(ctx context.Context, t *testing.T, ds *Datastore) {
appCfg, err := ds.AppConfig(ctx)
require.NoError(t, err)
prev := appCfg.MDM.WindowsEnabledAndConfigured
appCfg.MDM.WindowsEnabledAndConfigured = true
require.NoError(t, ds.SaveAppConfig(ctx, appCfg))
t.Cleanup(func() {
bgCtx := context.Background()
cfg, err := ds.AppConfig(bgCtx)
require.NoError(t, err)
cfg.MDM.WindowsEnabledAndConfigured = prev
require.NoError(t, ds.SaveAppConfig(bgCtx, cfg))
})
}
func testDeleteMDMProfilesCancelsInstalls(t *testing.T, ds *Datastore) {
ctx := t.Context()
SetTestABMAssets(t, ds, "fleet")
enableWindowsMDMForReconcileTest(ctx, t, ds)
// create some Apple and Windows profiles and declaration
appleProfs := []*fleet.MDMAppleConfigProfile{
configProfileForTest(t, "A1", "A1", uuid.NewString()),
configProfileForTest(t, "A2", "A2", uuid.NewString()),
configProfileForTest(t, "A3", "A3", uuid.NewString()),
}
windowsProfs := []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "W1", "W1"),
windowsConfigProfileForTest(t, "W2", "W2"),
}
appleDecls := []*fleet.MDMAppleDeclaration{
declForTest("D1", "D1", "{}"),
declForTest("D2", "D2", "{}"),
}
androidProfs := []*fleet.MDMAndroidConfigProfile{
androidConfigProfileForTest(t, "G1", nil),
androidConfigProfileForTest(t, "G2", nil),
}
_, err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, windowsProfs, appleDecls, androidProfs, nil)
require.NoError(t, err)
// collect the profiles in a lookup table by name
profNameToProf := make(map[string]*fleet.MDMConfigProfilePayload)
profs, _, err := ds.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{})
require.NoError(t, err)
for _, prof := range profs {
profNameToProf[prof.Name] = prof
}
// delete all kinds of profiles without any host impacted
err = ds.DeleteMDMAppleConfigProfile(ctx, profNameToProf["A1"].ProfileUUID)
require.NoError(t, err)
err = ds.DeleteMDMWindowsConfigProfile(ctx, profNameToProf["W1"].ProfileUUID)
require.NoError(t, err)
err = ds.DeleteMDMAppleDeclaration(ctx, profNameToProf["D1"].ProfileUUID)
require.NoError(t, err)
err = ds.DeleteMDMAndroidConfigProfile(ctx, profNameToProf["G1"].ProfileUUID)
require.NoError(t, err)
// create some macOS, Windows and android hosts
host1 := test.NewHost(t, ds, "host1", "1", "h1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "h2key", "host2uuid", time.Now())
nanoEnroll(t, ds, host1, false)
nanoEnroll(t, ds, host2, false)
host3 := test.NewHost(t, ds, "host3", "3", "h3key", "host3uuid", time.Now())
host3.Platform = "windows"
err = ds.UpdateHost(ctx, host3)
require.NoError(t, err)
windowsEnroll(t, ds, host3)
host4 := test.NewHost(t, ds, "host4", "4", "h4key", "host4uuid", time.Now())
host4.Platform = "windows"
err = ds.UpdateHost(ctx, host4)
require.NoError(t, err)
windowsEnroll(t, ds, host4)
host5 := test.NewHost(t, ds, "host5", "5", "h5key", "host5uuid", time.Now())
host5.Platform = "android"
err = ds.UpdateHost(ctx, host5)
require.NoError(t, err)
host6 := test.NewHost(t, ds, "host6", "6", "h6key", "host6uuid", time.Now())
host6.Platform = "android"
err = ds.UpdateHost(ctx, host6)
require.NoError(t, err)
for _, h := range []*fleet.Host{host1, host2, host3, host4, host5, host6} {
err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://fleetdm.com", false, fleet.WellKnownMDMFleet, "", false)
require.NoError(t, err)
}
// set the declaration as pending install on host1, installed on host2
forceSetAppleHostDeclarationStatus(t, ds, host1.UUID, test.ToMDMAppleDecl(profNameToProf["D2"]), fleet.MDMOperationTypeInstall, "")
forceSetAppleHostDeclarationStatus(t, ds, host2.UUID, test.ToMDMAppleDecl(profNameToProf["D2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
err = ds.DeleteMDMAppleDeclaration(ctx, profNameToProf["D2"].ProfileUUID)
require.NoError(t, err)
assertHostProfileOpStatus(t, ds, host1.UUID)
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
// set the Windows profile as pending install on host3, installed on host4
forceSetWindowsHostProfileStatus(t, ds, host3.UUID, test.ToMDMWindowsConfigProfile(profNameToProf["W2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetWindowsHostProfileStatus(t, ds, host4.UUID, test.ToMDMWindowsConfigProfile(profNameToProf["W2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
assertHostProfileOpStatus(t, ds, host3.UUID,
hostProfileOpStatus{profNameToProf["W2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host4.UUID,
hostProfileOpStatus{profNameToProf["W2"].ProfileUUID, fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
err = ds.DeleteMDMWindowsConfigProfile(ctx, profNameToProf["W2"].ProfileUUID)
require.NoError(t, err)
// Windows profile removal is async now (#46993): the delete retains the profile content, and the profile-manager cron flips the
// surviving host rows to remove+pending and enqueues the <Delete>.
require.NoError(t, service.ReconcileWindowsProfiles(ctx, ds, ds.logger))
assertHostProfileOpStatus(t, ds, host3.UUID,
hostProfileOpStatus{profNameToProf["W2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
assertHostProfileOpStatus(t, ds, host4.UUID,
hostProfileOpStatus{profNameToProf["W2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
// set the android profile as pending install on host 5 and installed on host 6
forceSetAndroidHostProfileStatus(t, ds, host5.UUID, test.ToMDMAndroidConfigProfile(profNameToProf["G2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
forceSetAndroidHostProfileStatus(t, ds, host6.UUID, test.ToMDMAndroidConfigProfile(profNameToProf["G2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
assertHostProfileOpStatus(t, ds, host5.UUID,
hostProfileOpStatus{profNameToProf["G2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host6.UUID,
hostProfileOpStatus{profNameToProf["G2"].ProfileUUID, fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
err = ds.DeleteMDMAndroidConfigProfile(ctx, profNameToProf["G2"].ProfileUUID)
require.NoError(t, err)
// We can't fully delete failed install profiles, as they might have values sent to the device even if some is overriden.
// So check for pending, remove instead of fully removed
assertHostProfileOpStatus(t, ds, host5.UUID,
hostProfileOpStatus{profNameToProf["G2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
assertHostProfileOpStatus(t, ds, host6.UUID,
hostProfileOpStatus{profNameToProf["G2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
commander, _ := createMDMAppleCommanderAndStorage(t, ds)
// set the Apple profile as pending install on host1, installed on host2
forceSetAppleHostProfileStatus(t, ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToProf["A2"]), fleet.MDMOperationTypeInstall, "")
forceSetAppleHostProfileStatus(t, ds, host2.UUID, test.ToMDMAppleConfigProfile(profNameToProf["A2"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
// enqueue the corresponding command for the installed profile
cmdUUID := uuid.New().String()
err = commander.InstallProfile(ctx, []string{host2.UUID}, appleProfs[1].Mobileconfig, cmdUUID, "")
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET command_uuid = ? WHERE host_uuid = ? AND profile_uuid = ?`, cmdUUID, host2.UUID, profNameToProf["A2"].ProfileUUID)
return err
})
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profNameToProf["A2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["A2"].ProfileUUID, fleet.MDMDeliveryVerifying, fleet.MDMOperationTypeInstall},
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
err = ds.DeleteMDMAppleConfigProfile(ctx, profNameToProf["A2"].ProfileUUID)
require.NoError(t, err)
assertHostProfileOpStatus(t, ds, host1.UUID)
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["A2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove},
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
// nano command is still active because it was already completed (verifying)
var active bool
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &active, `SELECT active FROM nano_enrollment_queue WHERE id = ? AND command_uuid = ?`, host2.UUID, cmdUUID)
})
require.True(t, active)
// set the Apple profile as actually pending install (not NULL) on host1
forceSetAppleHostProfileStatus(t, ds, host1.UUID, test.ToMDMAppleConfigProfile(profNameToProf["A3"]), fleet.MDMOperationTypeInstall, fleet.MDMDeliveryPending)
// enqueue the corresponding command for the installed profile
cmdUUID = uuid.New().String()
err = commander.InstallProfile(ctx, []string{host1.UUID}, appleProfs[2].Mobileconfig, cmdUUID, "")
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET command_uuid = ? WHERE host_uuid = ? AND profile_uuid = ?`, cmdUUID, host1.UUID, profNameToProf["A3"].ProfileUUID)
return err
})
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profNameToProf["A3"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["A2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove},
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
err = ds.DeleteMDMAppleConfigProfile(ctx, profNameToProf["A3"].ProfileUUID)
require.NoError(t, err)
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profNameToProf["A3"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profNameToProf["A2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove},
hostProfileOpStatus{profNameToProf["D2"].ProfileUUID, fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
// nano command is now inactive because it was in a pending state and we want to prevent delivery
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &active, `SELECT active FROM nano_enrollment_queue WHERE id = ? AND command_uuid = ?`, host1.UUID, cmdUUID)
})
require.False(t, active)
// listing the MDM commands does not return the inactive one
cmds, _, _, err := ds.ListMDMCommands(ctx, fleet.TeamFilter{
User: test.UserAdmin,
IncludeObserver: true,
}, &fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}, Filters: fleet.MDMCommandFilters{HostIdentifier: host1.UUID}})
require.NoError(t, err)
require.Len(t, cmds, 0)
// Windows hosts have pending removes; cleanup should wipe them.
err = ds.CleanupAllHostMDMProfilesForPlatform(ctx, "windows")
require.NoError(t, err)
assertHostProfileOpStatus(t, ds, host3.UUID)
assertHostProfileOpStatus(t, ds, host4.UUID)
// Windows host_mdm rows must NOT be touched by global disable. Orbit's programmatic-unenrollment notification
// depends on host_mdm.enrolled = 1 to be able to tell the device to unenroll.
var winHostMDM struct {
Total int `db:"total"`
Enrolled int `db:"enrolled"`
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &winHostMDM,
`SELECT
COUNT(*) AS total,
COALESCE(SUM(CASE WHEN hmdm.enrolled = 1 THEN 1 ELSE 0 END), 0) AS enrolled
FROM host_mdm hmdm
JOIN hosts h ON h.id = hmdm.host_id
WHERE h.uuid IN (?, ?) AND h.platform = 'windows'`,
host3.UUID, host4.UUID)
})
require.Equal(t, 2, winHostMDM.Total, "Windows host_mdm rows must survive global disable")
require.Equal(t, 2, winHostMDM.Enrolled,
"Windows host_mdm.enrolled must be preserved so orbit can still send unenroll notifications")
// Apple hosts still have pending removes; verify they survived the Windows cleanup, then disable Apple MDM too.
appleProfsHost1, err := ds.GetHostMDMAppleProfiles(ctx, host1.UUID)
require.NoError(t, err)
require.NotEmpty(t, appleProfsHost1)
appleProfsHost2, err := ds.GetHostMDMAppleProfiles(ctx, host2.UUID)
require.NoError(t, err)
require.NotEmpty(t, appleProfsHost2)
err = ds.CleanupAllHostMDMProfilesForPlatform(ctx, "darwin")
require.NoError(t, err)
assertHostProfileOpStatus(t, ds, host1.UUID)
assertHostProfileOpStatus(t, ds, host2.UUID)
// Apple nano_enrollments must be soft-disabled (rows survive, enabled = 0) so the reconciler does not recreate
// pending rows when a new APNS cert is uploaded.
var appleEnrollments struct {
Total int `db:"total"`
Disabled int `db:"disabled"`
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &appleEnrollments,
`SELECT
COUNT(*) AS total,
COALESCE(SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END), 0) AS disabled
FROM nano_enrollments
WHERE id IN (?, ?)`,
host1.UUID, host2.UUID)
})
require.Positive(t, appleEnrollments.Total, "nano_enrollments rows must survive global disable, not be deleted")
require.Equal(t, appleEnrollments.Total, appleEnrollments.Disabled, "nano_enrollments rows must be flipped to enabled = 0")
}
// testDeleteTeamCancelsWindowsProfileInstalls verifies that when a team is
// deleted, <Delete> commands are generated for Windows profiles that were
// delivered to hosts. This ensures settings are actually removed from devices
// rather than silently orphaned.
func testDeleteTeamCancelsWindowsProfileInstalls(t *testing.T, ds *Datastore) {
ctx := t.Context()
enableWindowsMDMForReconcileTest(ctx, t, ds)
// Create a team with Windows profiles.
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "delete-team-test"})
require.NoError(t, err)
windowsProfs := []*fleet.MDMWindowsConfigProfile{
windowsConfigProfileForTest(t, "TW1", "TW1"),
windowsConfigProfileForTest(t, "TW2", "TW2"),
}
_, err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, windowsProfs, nil, nil, nil)
require.NoError(t, err)
// Collect profile UUIDs.
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, profs, 2)
profUUIDs := make([]string, len(profs))
for i, p := range profs {
profUUIDs[i] = p.ProfileUUID
}
// Create Windows hosts enrolled in MDM and assigned to the team.
host1 := test.NewHost(t, ds, "tw-host1", "tw1", "tw1key", "tw-host1-uuid", time.Now())
host1.Platform = "windows"
err = ds.UpdateHost(ctx, host1)
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID})))
host1.TeamID = &team.ID
windowsEnroll(t, ds, host1)
host2 := test.NewHost(t, ds, "tw-host2", "tw2", "tw2key", "tw-host2-uuid", time.Now())
host2.Platform = "windows"
err = ds.UpdateHost(ctx, host2)
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host2.ID})))
host2.TeamID = &team.ID
windowsEnroll(t, ds, host2)
// Simulate profiles delivered to both hosts (install + verified).
for _, h := range []*fleet.Host{host1, host2} {
for _, p := range profs {
forceSetWindowsHostProfileStatus(t, ds, h.UUID,
test.ToMDMWindowsConfigProfile(p),
fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
}
}
// Verify: 4 rows, all install+verified.
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profUUIDs[0], fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall},
hostProfileOpStatus{profUUIDs[1], fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profUUIDs[0], fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall},
hostProfileOpStatus{profUUIDs[1], fleet.MDMDeliveryVerified, fleet.MDMOperationTypeInstall})
// Delete the team. Removal is async now (#46993): this retains the profiles' content and moves the hosts to No team; the
// profile-manager cron then flips their rows and enqueues the <Delete> commands.
err = ds.DeleteTeam(ctx, team.ID)
require.NoError(t, err)
// Config profiles should be gone.
teamProfs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
require.NoError(t, err)
require.Len(t, teamProfs, 0)
require.NoError(t, service.ReconcileWindowsProfiles(ctx, ds, ds.logger))
// Host-profile rows should be remove+pending (not remove+NULL, not deleted).
assertHostProfileOpStatus(t, ds, host1.UUID,
hostProfileOpStatus{profUUIDs[0], fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove},
hostProfileOpStatus{profUUIDs[1], fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
assertHostProfileOpStatus(t, ds, host2.UUID,
hostProfileOpStatus{profUUIDs[0], fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove},
hostProfileOpStatus{profUUIDs[1], fleet.MDMDeliveryPending, fleet.MDMOperationTypeRemove})
// Verify <Delete> commands were enqueued.
var deleteCmdCount int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &deleteCmdCount, `
SELECT COUNT(*) FROM windows_mdm_command_queue wmcq
JOIN windows_mdm_commands wmc ON wmc.command_uuid = wmcq.command_uuid
WHERE wmc.raw_command LIKE '%<Delete>%'`)
})
// 2 profiles × 2 hosts = 4 queue entries (but command rows are shared per profile)
require.Equal(t, 4, deleteCmdCount, "expected 4 delete command queue entries (2 profiles × 2 hosts)")
}
func androidConfigProfileForTest(t *testing.T, name string, content map[string]any, labels ...*fleet.Label) *fleet.MDMAndroidConfigProfile {
if content == nil {
content = make(map[string]any)
}
content["name"] = name
rawJSON, err := json.Marshal(content)
require.NoError(t, err)
prof := &fleet.MDMAndroidConfigProfile{
Name: name,
RawJSON: rawJSON,
}
for _, lbl := range labels {
switch {
case strings.HasPrefix(lbl.Name, "exclude-"):
prof.LabelsExcludeAny = append(prof.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
case strings.HasPrefix(lbl.Name, "include-any-"):
prof.LabelsIncludeAny = append(prof.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
default:
prof.LabelsIncludeAll = append(prof.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: lbl.Name, LabelID: lbl.ID})
}
}
return prof
}
func forceSetWindowsHostProfileStatus(t *testing.T, ds *Datastore, hostUUID string, profile *fleet.MDMWindowsConfigProfile, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
ctx := t.Context()
// empty status string means set to NULL
var actualStatus *fleet.MDMDeliveryStatus
if status != "" {
actualStatus = &status
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_windows_profiles
(host_uuid, status, operation_type, command_uuid, profile_name, checksum, profile_uuid)
VALUES
(?, ?, ?, ?, ?, UNHEX(MD5(?)), ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type)
`,
hostUUID, actualStatus, operation, uuid.NewString(), profile.Name, profile.SyncML, profile.ProfileUUID)
return err
})
}
func forceSetAppleHostDeclarationStatus(t *testing.T, ds *Datastore, hostUUID string, profile *fleet.MDMAppleDeclaration, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
ctx := t.Context()
// empty status string means set to NULL
var actualStatus *fleet.MDMDeliveryStatus
if status != "" {
actualStatus = &status
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
token := uuid.New() // Generate binary UUID
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_apple_declarations
(declaration_identifier, host_uuid, status, operation_type, token, declaration_name, declaration_uuid)
VALUES
(?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type)
`,
profile.Identifier, hostUUID, actualStatus, operation, token[:], profile.Name, profile.DeclarationUUID)
return err
})
}
func forceSetAndroidHostProfileStatus(t *testing.T, ds *Datastore, hostUUID string, profile *fleet.MDMAndroidConfigProfile, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
ctx := t.Context()
// empty status string means set to NULL
var actualStatus *fleet.MDMDeliveryStatus
if status != "" {
actualStatus = &status
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_android_profiles
(host_uuid, status, operation_type, profile_name, profile_uuid)
VALUES
(?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type)
`,
hostUUID, actualStatus, operation, profile.Name, profile.ProfileUUID)
return err
})
}
func testEnqueueCommandWithName(t *testing.T, ds *Datastore) {
ctx := context.Background()
SetTestABMAssets(t, ds, "fleet")
// Create and enroll a macOS host
macH, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-host-cmd-name",
OsqueryHostID: new("osquery-macos-cmd-name"),
NodeKey: new("node-key-macos-cmd-name"),
UUID: uuid.NewString(),
Platform: "darwin",
HardwareSerial: "CMDNAME123",
})
require.NoError(t, err)
nanoEnroll(t, ds, macH, false)
commander, mdmStorage := createMDMAppleCommanderAndStorage(t, ds)
// Test 1: InstallProfile with a name
cmdUUID1 := uuid.New().String()
mc := mobileconfig.Mobileconfig(fmt.Appendf(nil, `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array/>
<key>PayloadDisplayName</key>
<string>Test Profile</string>
<key>PayloadIdentifier</key>
<string>com.test.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>%s</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>`, uuid.New().String()))
err = commander.InstallProfile(ctx, []string{macH.UUID}, mc, cmdUUID1, "Test Profile Name")
require.NoError(t, err)
// Verify name in DB
var storedName sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &storedName, `SELECT name FROM nano_commands WHERE command_uuid = ?`, cmdUUID1)
})
require.True(t, storedName.Valid)
require.Equal(t, "Test Profile Name", storedName.String)
// Also verify via ListMDMCommands
cmds, _, _, err := ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}})
require.NoError(t, err)
require.Len(t, cmds, 1)
require.NotNil(t, cmds[0].Name)
require.Equal(t, "Test Profile Name", *cmds[0].Name)
// Test 2: EnqueueCommand without a name
cmdUUID2 := uuid.New().String()
rawCmd := createRawAppleCmd("ProfileList", cmdUUID2)
err = commander.EnqueueCommand(ctx, []string{macH.UUID}, rawCmd)
require.NoError(t, err)
// Verify name is NULL in DB
var storedName2 sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &storedName2, `SELECT name FROM nano_commands WHERE command_uuid = ?`, cmdUUID2)
})
require.False(t, storedName2.Valid)
// Verify name is null in the API
// Verify ListMDMCommands also exposes nil Name for unnamed commands
cmds, _, _, err = ds.ListMDMCommands(ctx, fleet.TeamFilter{User: test.UserAdmin}, &fleet.MDMCommandListOptions{ListOptions: fleet.ListOptions{PerPage: 100}})
require.NoError(t, err)
require.Len(t, cmds, 2)
gotByUUID := make(map[string]*fleet.MDMCommand, len(cmds))
for _, c := range cmds {
gotByUUID[c.CommandUUID] = c
}
require.Equal(t, "Test Profile Name", *gotByUUID[cmdUUID1].Name)
require.Nil(t, gotByUUID[cmdUUID2].Name)
// Test 3: Name exactly 255 characters — should NOT be truncated
cmdUUID3 := uuid.New().String()
name255 := strings.Repeat("a", 255)
rawXML3 := createRawAppleCmd("InstallProfile", cmdUUID3)
decodedCmd3, err := mdm.DecodeCommand([]byte(rawXML3))
require.NoError(t, err)
_, err = mdmStorage.EnqueueCommand(ctx, []string{macH.UUID}, &mdm.CommandWithSubtype{
Command: *decodedCmd3,
Subtype: mdm.CommandSubtypeNone,
Name: name255,
})
require.NoError(t, err)
var storedName3 sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &storedName3, `SELECT name FROM nano_commands WHERE command_uuid = ?`, cmdUUID3)
})
require.True(t, storedName3.Valid)
require.Equal(t, name255, storedName3.String)
require.Len(t, []rune(storedName3.String), 255)
// Test 4: Name longer than 255 ASCII characters — should be truncated to 255
cmdUUID4 := uuid.New().String()
name300 := strings.Repeat("b", 300)
rawXML4 := createRawAppleCmd("InstallProfile", cmdUUID4)
decodedCmd4, err := mdm.DecodeCommand([]byte(rawXML4))
require.NoError(t, err)
_, err = mdmStorage.EnqueueCommand(ctx, []string{macH.UUID}, &mdm.CommandWithSubtype{
Command: *decodedCmd4,
Subtype: mdm.CommandSubtypeNone,
Name: name300,
})
require.NoError(t, err)
var storedName4 sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &storedName4, `SELECT name FROM nano_commands WHERE command_uuid = ?`, cmdUUID4)
})
require.True(t, storedName4.Valid)
require.Equal(t, strings.Repeat("b", 255), storedName4.String)
require.Len(t, []rune(storedName4.String), 255)
// Test 5: Name with multi-byte utf8mb4 characters exceeding 255 — should truncate to 255 runes (not bytes)
cmdUUID5 := uuid.New().String()
// Each emoji is a single rune but 4 bytes in utf8mb4. Build a 300-rune string.
nameMultibyte := strings.Repeat("🍎", 300)
require.Len(t, []rune(nameMultibyte), 300)
rawXML5 := createRawAppleCmd("InstallProfile", cmdUUID5)
decodedCmd5, err := mdm.DecodeCommand([]byte(rawXML5))
require.NoError(t, err)
_, err = mdmStorage.EnqueueCommand(ctx, []string{macH.UUID}, &mdm.CommandWithSubtype{
Command: *decodedCmd5,
Subtype: mdm.CommandSubtypeNone,
Name: nameMultibyte,
})
require.NoError(t, err)
var storedName5 sql.NullString
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &storedName5, `SELECT name FROM nano_commands WHERE command_uuid = ?`, cmdUUID5)
})
require.True(t, storedName5.Valid)
require.Equal(t, strings.Repeat("🍎", 255), storedName5.String)
require.Len(t, []rune(storedName5.String), 255)
}
func testCleanUpMDMManagedCertificates(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("host0-osquery-id"),
NodeKey: ptr.String("host0-node-key"),
UUID: "host0-test-mdm-profiles",
Hostname: "hostname0",
})
require.NoError(t, err)
t.Run("non matching host profile record", func(t *testing.T) {
badProfileUUID := uuid.NewString()
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_mdm_managed_certificates (host_uuid, profile_uuid) VALUES (?, ?)
`, host.UUID, badProfileUUID)
if err != nil {
return err
}
return nil
})
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
badProfileUUID)
})
require.Equal(t, badProfileUUID, uid)
// Cleanup should delete the above orphaned record
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
err = ExecAdhocSQLWithError(ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
badProfileUUID)
})
require.ErrorIs(t, err, sql.ErrNoRows)
})
t.Run("valid windows record stays", func(t *testing.T) {
t.Cleanup(func() {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
DELETE FROM host_mdm_managed_certificates;
DELETE FROM host_mdm_windows_profiles;
`)
return err
})
})
windowsProfileUUID := uuid.NewString()
err := ds.BulkUpsertMDMWindowsHostProfiles(ctx, []*fleet.MDMWindowsBulkUpsertHostProfilePayload{{
ProfileUUID: windowsProfileUUID,
HostUUID: host.UUID,
Checksum: []byte("gibberish"),
OperationType: fleet.MDMOperationTypeInstall,
}})
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_mdm_managed_certificates (host_uuid, profile_uuid) VALUES (?, ?)
`, host.UUID, windowsProfileUUID)
return err
})
// Validate record stays after cleanup
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
windowsProfileUUID)
})
require.Equal(t, windowsProfileUUID, uid)
})
t.Run("valid apple record stays", func(t *testing.T) {
t.Cleanup(func() {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
DELETE FROM host_mdm_managed_certificates;
DELETE FROM host_mdm_apple_profiles;
`)
return err
})
})
appleProfileUUID := uuid.NewString()
err := ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: appleProfileUUID,
HostUUID: host.UUID,
Checksum: []byte("gibberish"),
Scope: fleet.PayloadScopeSystem,
OperationType: fleet.MDMOperationTypeInstall,
}})
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_mdm_managed_certificates (host_uuid, profile_uuid) VALUES (?, ?)
`, host.UUID, appleProfileUUID)
return err
})
// Validate record stays after cleanup
err = ds.CleanUpMDMManagedCertificates(ctx)
require.NoError(t, err)
var uid string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &uid, `SELECT profile_uuid FROM host_mdm_managed_certificates WHERE profile_uuid = ?`,
appleProfileUUID)
})
require.Equal(t, appleProfileUUID, uid)
})
}
func testProfileHasACMEPayloadForCommand(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("acme-probe-osq"),
NodeKey: ptr.String("acme-probe-nk"),
UUID: "acme-probe-host-uuid",
Hostname: "acme-probe-host",
Platform: "darwin",
})
require.NoError(t, err)
mkProfile := func(t *testing.T, name string, mobileconfig []byte) string {
t.Helper()
teamID := uint(0)
profileUUID := uuid.NewString()
stmt := `
INSERT INTO mdm_apple_configuration_profiles
(profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())`
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, stmt,
profileUUID, teamID, name, name, mobileconfig, []byte("0123456789abcdef"))
return err
})
return profileUUID
}
mkHostProfileLink := func(t *testing.T, hostUUID, profileUUID, commandUUID string) {
t.Helper()
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: profileUUID,
HostUUID: hostUUID,
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: commandUUID,
}}))
}
acmeXML := []byte(`<?xml version="1.0"?><plist><dict><key>PayloadContent</key><array><dict><key>PayloadType</key><string>com.apple.security.acme</string></dict></array></dict></plist>`)
scepXML := []byte(`<?xml version="1.0"?><plist><dict><key>PayloadContent</key><array><dict><key>PayloadType</key><string>com.apple.security.scep</string></dict></array></dict></plist>`)
t.Run("darwin host with ACME profile, no pending refetch", func(t *testing.T) {
profUUID := mkProfile(t, "acme-darwin", acmeXML)
cmdUUID := uuid.NewString()
mkHostProfileLink(t, host.UUID, profUUID, cmdUUID)
got, err := ds.ProfileHasACMEPayloadForCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.Equal(t, host.ID, got.HostID)
require.Equal(t, "darwin", got.Platform)
require.Equal(t, profUUID, got.ProfileUUID)
require.True(t, got.HasACMEPayload)
})
t.Run("darwin host with non-ACME profile reports has_acme_payload=false", func(t *testing.T) {
profUUID := mkProfile(t, "scep-darwin", scepXML)
cmdUUID := uuid.NewString()
mkHostProfileLink(t, host.UUID, profUUID, cmdUUID)
got, err := ds.ProfileHasACMEPayloadForCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.Equal(t, "darwin", got.Platform)
require.False(t, got.HasACMEPayload)
})
t.Run("flag persists after the config profile is deleted", func(t *testing.T) {
// The RemoveProfile flow: the config profile is gone but the persisted
// flag must still report ACME.
profUUID := mkProfile(t, "acme-deleted", acmeXML)
cmdUUID := uuid.NewString()
mkHostProfileLink(t, host.UUID, profUUID, cmdUUID)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles WHERE profile_uuid = ?`, profUUID)
return err
})
got, err := ds.ProfileHasACMEPayloadForCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.Equal(t, "darwin", got.Platform)
require.True(t, got.HasACMEPayload, "flag must survive config profile deletion")
})
t.Run("remove-op upsert preserves the install-time flag", func(t *testing.T) {
profUUID := mkProfile(t, "acme-removeop", acmeXML)
cmdUUID := uuid.NewString()
mkHostProfileLink(t, host.UUID, profUUID, cmdUUID)
// Remove-op upsert after the config profile is gone: the subquery
// yields 0, but the ON DUPLICATE KEY UPDATE guard must preserve the flag.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM mdm_apple_configuration_profiles WHERE profile_uuid = ?`, profUUID)
return err
})
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: profUUID,
HostUUID: host.UUID,
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
OperationType: fleet.MDMOperationTypeRemove,
CommandUUID: cmdUUID,
}}))
got, err := ds.ProfileHasACMEPayloadForCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.True(t, got.HasACMEPayload, "remove-op upsert must not reset the flag")
})
t.Run("unknown command returns not found", func(t *testing.T) {
_, err := ds.ProfileHasACMEPayloadForCommand(ctx, host.UUID, "no-such-command")
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))
})
t.Run("unknown host returns not found", func(t *testing.T) {
profUUID := mkProfile(t, "acme-unknown-host", acmeXML)
cmdUUID := uuid.NewString()
mkHostProfileLink(t, host.UUID, profUUID, cmdUUID)
_, err := ds.ProfileHasACMEPayloadForCommand(ctx, "no-such-host", cmdUUID)
require.Error(t, err)
require.True(t, fleet.IsNotFound(err))
})
}
func testOktaCACleanupTargetForInstallCommand(t *testing.T, ds *Datastore) {
ctx := t.Context()
newHost := func(t *testing.T, suffix, platform string) *fleet.Host {
t.Helper()
h, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("okta-cleanup-osq-" + suffix),
NodeKey: ptr.String("okta-cleanup-nk-" + suffix),
UUID: "okta-cleanup-host-" + suffix,
Hostname: "okta-cleanup-" + suffix,
Platform: platform,
})
require.NoError(t, err)
return h
}
insertHostProfile := func(t *testing.T, hostUUID, identifier, commandUUID string) {
t.Helper()
// host_mdm_apple_profiles has no FK to mdm_apple_configuration_profiles,
// so we can insert the host-profile row directly with a fresh
// profile_uuid per sub-test and avoid the (team_id, identifier)
// unique-key conflict that comes from reusing the Okta CA identifier.
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: uuid.NewString(),
ProfileIdentifier: identifier,
HostUUID: hostUUID,
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeUser,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: commandUUID,
}}))
}
t.Run("okta CA profile + per-user enrollment present: returns target", func(t *testing.T) {
host := newHost(t, "happy", "darwin")
nanoEnroll(t, ds, host, true) // creates Device + User enrollment with user_short_name = "alice"
cmdUUID := uuid.NewString()
insertHostProfile(t, host.UUID, fleet.ConditionalAccessOktaProfileIdentifier, cmdUUID)
got, ok, err := ds.OktaCACleanupTargetForInstallCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, host.ID, got.HostID)
require.Equal(t, "alice", got.UserShortName)
})
t.Run("non-Okta profile identifier: ok=false", func(t *testing.T) {
host := newHost(t, "wrong-id", "darwin")
nanoEnroll(t, ds, host, true)
cmdUUID := uuid.NewString()
insertHostProfile(t, host.UUID, "com.example.unrelated", cmdUUID)
_, ok, err := ds.OktaCACleanupTargetForInstallCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.False(t, ok)
})
t.Run("okta profile + only device-channel enrollment: ok=false", func(t *testing.T) {
host := newHost(t, "no-user-chan", "darwin")
nanoEnroll(t, ds, host, false) // Device-only enrollment, no nano_users row
cmdUUID := uuid.NewString()
insertHostProfile(t, host.UUID, fleet.ConditionalAccessOktaProfileIdentifier, cmdUUID)
_, ok, err := ds.OktaCACleanupTargetForInstallCommand(ctx, host.UUID, cmdUUID)
require.NoError(t, err)
require.False(t, ok)
})
t.Run("unknown command: ok=false, no error", func(t *testing.T) {
host := newHost(t, "no-cmd", "darwin")
nanoEnroll(t, ds, host, true)
_, ok, err := ds.OktaCACleanupTargetForInstallCommand(ctx, host.UUID, "no-such-cmd")
require.NoError(t, err)
require.False(t, ok)
})
}
func testRenewMDMManagedCertificatesNullType(t *testing.T, ds *Datastore) {
ctx := t.Context()
host, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
OsqueryHostID: ptr.String("renew-null-osq"),
NodeKey: ptr.String("renew-null-nk"),
UUID: "renew-null-host-uuid",
Hostname: "renew-null-host",
Platform: "darwin",
})
require.NoError(t, err)
// Helper: create an Apple config profile + a host_mdm_apple_profiles row
// in 'verified' state (eligible for renewal cron resend) and an associated
// host_mdm_managed_certificates row with the given type and an expiring
// not_valid_after. Returns the profile UUID.
mkExpiringRow := func(t *testing.T, name string, certType *string, caName string) string {
t.Helper()
profileUUID := uuid.NewString()
// Insert mdm_apple_configuration_profiles row.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO mdm_apple_configuration_profiles
(profile_uuid, team_id, identifier, name, mobileconfig, checksum, uploaded_at)
VALUES (?, 0, ?, ?, ?, ?, NOW())`,
profileUUID, name, name, []byte("dummy"), []byte("0123456789abcdef"))
return err
})
// Insert host_mdm_apple_profiles row in verified state, eligible for renewal.
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
ProfileUUID: profileUUID,
ProfileIdentifier: name,
ProfileName: name,
HostUUID: host.UUID,
Status: &fleet.MDMDeliveryVerified,
OperationType: fleet.MDMOperationTypeInstall,
CommandUUID: "cmd-" + profileUUID,
Checksum: []byte("0123456789abcdef"),
Scope: fleet.PayloadScopeSystem,
}}))
// Insert host_mdm_managed_certificates row with cert that expires soon
// (within the renewal cron's 30-day threshold, validity_period > 30).
notValidBefore := time.Now().AddDate(-1, 0, 0) // 1 year ago
notValidAfter := time.Now().AddDate(0, 0, 5) // 5 days from now
serial := "0000000000000000000000000000000000000001"
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_mdm_managed_certificates
(host_uuid, profile_uuid, ca_name, type,
not_valid_before, not_valid_after, serial)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
host.UUID, profileUUID, caName, certType, notValidBefore, notValidAfter, serial)
return err
})
return profileUUID
}
ndesStr := "ndes"
// Two rows expiring on the same schedule: one with NULL type
// (non-proxied flow) and one with type='ndes' (proxied flow).
// Both buckets must be picked up by the renewal cron.
nullProfile := mkExpiringRow(t, "null-prof", nil, "non-proxied-ca")
ndesProfile := mkExpiringRow(t, "ndes-prof", &ndesStr, "ndes-ca")
// Sanity: both start as 'verified'.
for _, profUUID := range []string{nullProfile, ndesProfile} {
var status *string
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &status, `
SELECT status FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND profile_uuid = ?`,
host.UUID, profUUID))
require.NotNil(t, status)
require.Equal(t, fleet.MDMDeliveryVerified, fleet.MDMDeliveryStatus(*status))
}
require.NoError(t, ds.RenewMDMManagedCertificates(ctx))
// Both should now have status=NULL (queued for resend).
for _, profUUID := range []string{nullProfile, ndesProfile} {
var status *string
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &status, `
SELECT status FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND profile_uuid = ?`,
host.UUID, profUUID))
require.Nil(t, status, "profile %s should be queued for resend", profUUID)
}
// Verify the read paths handle NULL `type` cleanly. The struct fields
// `MDMManagedCertificate.Type` and `HostMDMCertificateProfile.Type` are
// `CAConfigAssetType` (a string alias), not pointers. sqlx scans a NULL
// column into a string-aliased field as the empty string — no error, no
// special-case handling needed. This is the convention used throughout the
// non-proxied flow: NULL in the column == zero value in Go.
listed, err := ds.ListHostMDMManagedCertificates(ctx, host.UUID)
require.NoError(t, err, "ListHostMDMManagedCertificates must round-trip rows with NULL type")
var sawNullRow, sawNDESRow bool
for _, row := range listed {
switch row.ProfileUUID {
case nullProfile:
sawNullRow = true
require.Equal(t, fleet.CAConfigAssetType(""), row.Type,
"NULL type column should scan to empty CAConfigAssetType")
require.Equal(t, "non-proxied-ca", row.CAName)
case ndesProfile:
sawNDESRow = true
require.Equal(t, fleet.CAConfigNDES, row.Type,
"non-NULL type column should round-trip unchanged")
}
}
require.True(t, sawNullRow, "ListHostMDMManagedCertificates must return the NULL-type row")
require.True(t, sawNDESRow, "ListHostMDMManagedCertificates must return the existing ndes row")
// Same expectation via GetAppleHostMDMCertificateProfile, which returns
// HostMDMCertificateProfile (different struct, same nullable column).
nullProfileDetail, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, nullProfile, "non-proxied-ca")
require.NoError(t, err)
require.NotNil(t, nullProfileDetail)
require.Equal(t, fleet.CAConfigAssetType(""), nullProfileDetail.Type,
"HostMDMCertificateProfile.Type must scan a NULL column as empty string")
ndesProfileDetail, err := ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, ndesProfile, "ndes-ca")
require.NoError(t, err)
require.NotNil(t, ndesProfileDetail)
require.Equal(t, fleet.CAConfigNDES, ndesProfileDetail.Type)
}