Show configuration profile name and more fine-grained status (#42126)

Resolves #40177 and subissues.

# 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] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## 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)

- [sorta] 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

* **New Features**
* Profile names are now displayed alongside mobile device management
commands for installing or removing profiles. These names are visible in
command details modals and within device activity timelines.
* Added "NotNow" status for deferred profile commands, providing
improved transparency into which profiles are being managed and the
current status of profile installation or removal operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ian Littman 2026-04-09 12:46:11 -05:00 committed by GitHub
parent d2c485a5f7
commit da6cfd8e9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 514 additions and 74 deletions

View file

@ -0,0 +1,2 @@
* Added logging of profile names alongside MDM commands installing or removing them
* Added indication in the UI when a profile command was deferred ("NotNow" status)

View file

@ -9,6 +9,7 @@ const DEFAULT_COMMAND_MOCK: ICommand = {
updated_at: "2024-01-01T00:00:00Z",
request_type: "InstallProfile",
hostname: "default-hostname",
name: null,
};
export const createMockCommand = (overrides?: Partial<ICommand>): ICommand => ({
@ -47,6 +48,7 @@ const DEFAULT_COMMAND_RESULT_MOCK: ICommandResult = {
<Status>Acknowledged</Status>
<Message>Installation complete</Message>
</Result>`),
name: null,
};
/**

View file

@ -12,6 +12,7 @@ export interface ICommand {
updated_at: string;
request_type: string;
hostname: string;
name: string | null; // Profile name when command is for installing/removing a macOS profile
}
/**
@ -30,10 +31,9 @@ export interface ICommandResult {
payload: string;
/** Base64-encoded string containing the MDM command response */
result: string;
name: string | null; // Profile name when command is for installing/removing a macOS profile
/** ResultsMetadata contains command-specific metadata.
* VPP install commands include a "software_installed" boolean and
* "vpp_verify_timeout_seconds" integer. */
results_metadata?: Record<string, unknown>;
}
export type IMDMCommandResult = ICommandResult;

View file

@ -11,12 +11,12 @@ import commandApi, {
IGetHostCommandResultsQueryKey,
} from "services/entities/command";
import InputField from "components/forms/fields/InputField";
import Modal from "components/Modal";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import IconStatusMessage from "components/IconStatusMessage";
import { IconNames } from "components/icons";
import Textarea from "components/Textarea";
import ModalFooter from "components/ModalFooter";
import Button from "components/buttons/Button";
@ -50,12 +50,19 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => {
})})`
: null;
const namePart = result.name ? (
<>
{" "}
for <b>{result.name}</b>
</>
) : null;
switch (result.status) {
case "CommandFormatError":
case "Error":
return (
<span>
The <b>{result.request_type}</b> command failed on{" "}
The <b>{result.request_type}</b> command{namePart} failed on{" "}
<b>{result.hostname}</b>
{displayTime}.
</span>
@ -64,7 +71,7 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => {
case "Acknowledged":
return (
<span>
The <b>{result.request_type}</b> command ran on{" "}
The <b>{result.request_type}</b> command{namePart} was acknowledged by{" "}
<b>{result.hostname}</b>
{displayTime}.
</span>
@ -73,15 +80,15 @@ const getStatusMessage = (result: ICommandResult): React.ReactNode => {
case "Pending":
return (
<span>
The <b>{result.request_type}</b> command is running or will run on{" "}
<b>{result.hostname}</b> when it comes online.
The <b>{result.request_type}</b> command{namePart} is pending on{" "}
<b>{result.hostname}</b>.
</span>
);
case "NotNow":
return (
<span>
The <b>{result.request_type}</b> command didn&apos;t run on{" "}
The <b>{result.request_type}</b> command{namePart} is deferred on{" "}
<b>{result.hostname}</b> because the host was locked or was running on
battery power while in Power Nap. Fleet will try again.
</span>
@ -135,21 +142,26 @@ const ModalContent = ({
message={getStatusMessage(result)}
/>
{!!result.payload && (
<Textarea label="Request payload:" variant="code">
{result.payload}
</Textarea>
<InputField
type="textarea"
label="Request payload:"
value={result.payload}
readOnly
enableCopy
/>
)}
{!!result.result && (
<Textarea
<InputField
type="textarea"
label={
<>
Response from <b>{result.hostname}</b>:
</>
}
variant="code"
>
{result.result}
</Textarea>
value={result.result}
readOnly
enableCopy
/>
)}
</div>
);

View file

@ -4,15 +4,16 @@
flex-direction: column;
gap: $pad-large;
padding-bottom: $pad-small;
.textarea-wrapper {
gap: $pad-small;
}
}
// shortening the hight of the textarea
// so the modal height stays within the viewport
.textarea--code {
.input-field__textarea {
// shortening the height of the textarea
// so the modal height stays within the viewport
max-height: 200px;
height: auto;
field-sizing: content;
overflow-y: auto;
background-color: $ui-off-white;
font-family: "SourceCodePro", $monospace;
}
}

View file

@ -18,27 +18,48 @@ interface ICommandItemProps {
onShowDetails: ShowCommandDetailsHandler;
}
const CommandItem = ({ command, onShowDetails }: ICommandItemProps) => {
const { command_status, request_type, updated_at } = command;
const isProfileCommand = (requestType: string): boolean =>
requestType === "InstallProfile" || requestType === "RemoveProfile";
const getStatusText = (command: ICommand): string => {
const { command_status, status } = command;
// Differentiate NotNow from regular Pending
if (status === "NotNow") {
return "is deferred";
}
let statusVerb = "";
switch (command_status) {
case "pending":
statusVerb = "will run";
break;
case "ran":
return "is pending";
case "failed":
statusVerb = command_status;
break;
return "failed";
case "ran":
default:
statusVerb = "ran";
return "was acknowledged";
}
};
const CommandItem = ({ command, onShowDetails }: ICommandItemProps) => {
const { request_type, updated_at, name } = command;
const statusText = getStatusText(command);
const onShowCommandDetails = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onShowDetails(command);
};
const activityText = name ? (
<>
The <b>{request_type}</b> command for <b>{name}</b> {statusText}.
</>
) : (
<>
The <b>{request_type}</b> command {statusText}.
</>
);
return (
<FeedListItem
className={baseClass}
@ -47,7 +68,7 @@ const CommandItem = ({ command, onShowDetails }: ICommandItemProps) => {
createdAt={new Date(updated_at)}
onClickFeedItem={onShowCommandDetails}
>
The <b>{request_type}</b> command {statusVerb}.
{activityText}
</FeedListItem>
);
};

View file

@ -1032,7 +1032,8 @@ SELECT
ncr.result,
ncr.updated_at,
nc.request_type,
nc.command as payload
nc.command as payload,
nc.name
FROM
nano_command_results ncr
INNER JOIN
@ -1085,7 +1086,8 @@ SELECT
COALESCE(ncr.status, 'Pending') AS status,
request_type,
nc.command AS payload,
COALESCE(ncr.result, '') AS result
COALESCE(ncr.result, '') AS result,
nc.name
FROM
nano_enrollment_queue nq
JOIN nano_commands nc ON nq.command_uuid = nc.command_uuid
@ -1130,7 +1132,8 @@ SELECT
COALESCE(nvq.result_updated_at, nvq.created_at) as updated_at,
nvq.request_type,
h.hostname,
h.team_id
h.team_id,
nvq.name
FROM
nano_view_queue nvq
INNER JOIN

View file

@ -48,7 +48,8 @@ SELECT
COALESCE(nvq.result_updated_at, nvq.created_at) as updated_at,
nvq.request_type as request_type,
h.hostname,
h.team_id
h.team_id,
nvq.name
FROM
nano_view_queue nvq
INNER JOIN
@ -67,7 +68,8 @@ SELECT
COALESCE(wmc.updated_at, wmc.created_at) as updated_at,
wmc.target_loc_uri as request_type,
h.hostname,
h.team_id
h.team_id,
NULL as name
FROM windows_mdm_commands wmc
LEFT JOIN windows_mdm_command_queue wmcq ON wmcq.command_uuid = wmc.command_uuid
LEFT JOIN windows_mdm_command_results wmcr ON wmc.command_uuid = wmcr.command_uuid
@ -225,7 +227,8 @@ SELECT
WHEN COALESCE(NULLIF(ncr.status, ''), 'Pending') = 'Error' THEN 'failed'
ELSE 'pending'
END AS command_status,
request_type
request_type,
nc.name
FROM
nano_enrollment_queue nq
JOIN nano_commands nc ON nq.command_uuid = nc.command_uuid
@ -251,7 +254,8 @@ WHERE
wc.created_at AS updated_at,
'101' AS status,
'pending' AS command_status,
wc.target_loc_uri AS request_type
wc.target_loc_uri AS request_type,
NULL AS name
FROM
windows_mdm_command_queue wq
JOIN mdm_windows_enrollments mwe ON mwe.id = wq.enrollment_id
@ -287,7 +291,8 @@ WHERE
) AS UNSIGNED
) >= 400 THEN 'failed'
END AS command_status,
wc.target_loc_uri AS request_type
wc.target_loc_uri AS request_type,
NULL AS name
FROM
windows_mdm_command_results wcr
JOIN mdm_windows_enrollments mwe ON mwe.id = wcr.enrollment_id

View file

@ -57,6 +57,7 @@ func TestMDMShared(t *testing.T) {
{"TestDeleteMDMProfilesCancelsInstalls", testDeleteMDMProfilesCancelsInstalls},
{"TestDeleteTeamCancelsWindowsProfileInstalls", testDeleteTeamCancelsWindowsProfileInstalls},
{"TestCleanUpMDMManagedCertificates", testCleanUpMDMManagedCertificates},
{"TestEnqueueCommandWithName", testEnqueueCommandWithName},
}
for _, c := range cases {
@ -9142,7 +9143,7 @@ func testDeleteMDMProfilesCancelsInstalls(t *testing.T, ds *Datastore) {
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)
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)
@ -9173,7 +9174,7 @@ func testDeleteMDMProfilesCancelsInstalls(t *testing.T, ds *Datastore) {
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)
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)
@ -9394,6 +9395,155 @@ func forceSetAndroidHostProfileStatus(t *testing.T, ds *Datastore, hostUUID stri
})
}
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{})
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{})
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()

View file

@ -0,0 +1,57 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20260409153713, Down_20260409153713)
}
func Up_20260409153713(tx *sql.Tx) error {
if !columnExists(tx, "nano_commands", "name") {
_, err := tx.Exec(`
ALTER TABLE nano_commands ADD COLUMN name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL`)
if err != nil {
return fmt.Errorf("failed to add nano_commands.name column: %w", err)
}
}
// Recreate the view to include the new name column
_, err := tx.Exec(`
CREATE OR REPLACE SQL SECURITY INVOKER VIEW nano_view_queue AS
SELECT
q.id COLLATE utf8mb4_unicode_ci AS id,
q.created_at,
q.active,
q.priority,
c.command_uuid COLLATE utf8mb4_unicode_ci AS command_uuid,
c.request_type COLLATE utf8mb4_unicode_ci AS request_type,
c.command COLLATE utf8mb4_unicode_ci AS command,
c.name AS name,
r.updated_at AS result_updated_at,
r.status COLLATE utf8mb4_unicode_ci AS status,
r.result COLLATE utf8mb4_unicode_ci AS result
FROM
nano_enrollment_queue AS q
INNER JOIN nano_commands AS c
ON q.command_uuid = c.command_uuid
LEFT JOIN nano_command_results r
ON r.command_uuid = q.command_uuid AND r.id = q.id
ORDER BY
q.priority DESC,
q.created_at;
`)
if err != nil {
return fmt.Errorf("failed to recreate nano_view_queue with name column: %w", err)
}
return nil
}
func Down_20260409153713(_ *sql.Tx) error {
return nil
}

File diff suppressed because one or more lines are too long

View file

@ -30,8 +30,8 @@ var (
)
type MDMAppleCommandIssuer interface {
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string, name string) error
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string, name string) error
DeviceLock(ctx context.Context, host *Host, uuid string) (unlockPIN string, err error)
EnableLostMode(ctx context.Context, host *Host, commandUUID string, orgName string) error
DisableLostMode(ctx context.Context, host *Host, commandUUID string) error
@ -631,6 +631,8 @@ type MDMAppleCommand struct {
// to authorize the user to see the command, it is not returned as part of
// the response payload.
TeamID *uint `json:"-" db:"team_id"`
// Name is the optional human-readable name of the command, used for indicating the profile added/removed, if any.
Name *string `json:"name" db:"name"`
}
// MDMAppleSetupAssistant represents the setup assistant set for a given team

View file

@ -16,6 +16,7 @@ import (
type CmdTarget struct {
CmdUUID string
ProfileIdentifier string
ProfileName string
EnrollmentIDs []string
}

View file

@ -374,6 +374,8 @@ type MDMCommandResult struct {
Hostname string `json:"hostname" db:"-"`
// Payload is the contents of the command
Payload []byte `json:"payload" db:"payload"`
// Name is the optional human-readable name of the command, currently used for profile name when adding/removing
Name *string `json:"name" db:"name"`
// ResultsMetadata contains command-specific metadata.
// VPP install commands include a "software_installed" boolean and
// "vpp_verify_timeout_seconds" integer.
@ -400,6 +402,8 @@ type MDMCommand struct {
// to authorize the user to see the command, it is not returned as part of
// the response payload.
TeamID *uint `json:"-" db:"team_id"`
// Name is the optional human-readable name of the command, currently used for profile name when adding/removing
Name *string `json:"name" db:"name"`
// CommandStatus is the fleet computed field representing the status of the command
// based on the MDM protocol status
CommandStatus MDMCommandStatusFilter `json:"command_status" db:"command_status"`

View file

@ -45,12 +45,16 @@ func NewMDMAppleCommander(mdmStorage fleet.MDMAppleStore, mdmPushService nanomdm
// InstallProfile sends the homonymous MDM command to the given hosts, it also
// takes care of the base64 encoding of the provided profile bytes.
func (svc *MDMAppleCommander) InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error {
func (svc *MDMAppleCommander) InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string, name string) error {
raw, err := svc.SignAndEncodeInstallProfile(ctx, profile, uuid)
if err != nil {
return err
}
err = svc.EnqueueCommand(ctx, hostUUIDs, raw)
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
return ctxerr.Wrap(ctx, err, "decoding InstallProfile command")
}
err = svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone, name)
return ctxerr.Wrap(ctx, err, "commander install profile")
}
@ -80,7 +84,7 @@ func (svc *MDMAppleCommander) SignAndEncodeInstallProfile(ctx context.Context, p
}
// RemoveProfile sends the homonymous MDM command to the given hosts.
func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []string, profileIdentifier string, uuid string) error {
func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []string, profileIdentifier string, uuid string, name string) error {
raw := fmt.Sprintf(`<?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">
@ -96,7 +100,11 @@ func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []str
</dict>
</dict>
</plist>`, uuid, profileIdentifier)
err := svc.EnqueueCommand(ctx, hostUUIDs, raw)
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
return ctxerr.Wrap(ctx, err, "decoding RemoveProfile command")
}
err = svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone, name)
return ctxerr.Wrap(ctx, err, "commander remove profile")
}
@ -193,7 +201,7 @@ func (svc *MDMAppleCommander) EnableLostMode(ctx context.Context, host *fleet.Ho
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
return ctxerr.Wrap(ctx, err, "decoding command")
return ctxerr.Wrap(ctx, err, "decoding EnableLostMode command")
}
if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, ""); err != nil {
@ -263,7 +271,7 @@ func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host,
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
return ctxerr.Wrap(ctx, err, "decoding command")
return ctxerr.Wrap(ctx, err, "decoding DeviceWipe command")
}
if err := svc.storage.EnqueueDeviceWipeCommand(ctx, host, cmd); err != nil {
@ -516,7 +524,7 @@ func (svc *MDMAppleCommander) ClearPasscode(ctx context.Context, hostUUIDs []str
}
cmd.Command.RequestType = fleet.AppleMDMCommandTypeClearPasscode
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone)
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone, "")
}
// EnqueueCommand takes care of enqueuing the commands and sending push
@ -531,14 +539,14 @@ func (svc *MDMAppleCommander) EnqueueCommand(ctx context.Context, hostUUIDs []st
return ctxerr.Wrap(ctx, err, "decoding command")
}
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone)
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone, "")
}
func (svc *MDMAppleCommander) enqueueAndNotify(ctx context.Context, hostUUIDs []string, cmd *mdm.Command,
subtype mdm.CommandSubtype,
subtype mdm.CommandSubtype, name string,
) error {
if _, err := svc.storage.EnqueueCommand(ctx, hostUUIDs,
&mdm.CommandWithSubtype{Command: *cmd, Subtype: subtype}); err != nil {
&mdm.CommandWithSubtype{Command: *cmd, Subtype: subtype, Name: name}); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing command")
}
@ -551,7 +559,7 @@ func (svc *MDMAppleCommander) enqueueAndNotify(ctx context.Context, hostUUIDs []
// EnqueueCommandInstallProfileWithSecrets is a special case of EnqueueCommand that does not expand secret variables.
// Secret variables are expanded when the command is sent to the device, and secrets are never stored in the database unencrypted.
func (svc *MDMAppleCommander) EnqueueCommandInstallProfileWithSecrets(ctx context.Context, hostUUIDs []string,
rawCommand mobileconfig.Mobileconfig, commandUUID string,
rawCommand mobileconfig.Mobileconfig, commandUUID string, name string,
) error {
cmd := &mdm.Command{
CommandUUID: commandUUID,
@ -559,7 +567,7 @@ func (svc *MDMAppleCommander) EnqueueCommandInstallProfileWithSecrets(ctx contex
}
cmd.Command.RequestType = "InstallProfile"
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeProfileWithSecrets)
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeProfileWithSecrets, name)
}
func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs []string) error {

View file

@ -104,7 +104,7 @@ func TestMDMAppleCommander(t *testing.T) {
}
cmdUUID := uuid.New().String()
err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID)
err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID, "")
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
mdmStorage.EnqueueCommandFuncInvoked = false
@ -118,7 +118,7 @@ func TestMDMAppleCommander(t *testing.T) {
return nil, nil
}
cmdUUID = uuid.New().String()
err = cmdr.RemoveProfile(ctx, hostUUIDs, payloadIdentifier, cmdUUID)
err = cmdr.RemoveProfile(ctx, hostUUIDs, payloadIdentifier, cmdUUID, "")
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
mdmStorage.EnqueueCommandFuncInvoked = false
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
@ -673,3 +673,140 @@ func TestMDMAppleCommanderClearPasscode(t *testing.T) {
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
}
func TestMDMAppleCommanderPassesCommandName(t *testing.T) {
ctx := context.Background()
mdmStorage := &mdmmock.MDMAppleStore{}
pushFactory, _ := newMockAPNSPushProviderFactory()
pusher := nanomdm_pushsvc.New(
mdmStorage,
mdmStorage,
pushFactory,
stdlogfmt.New(),
)
cmdr := NewMDMAppleCommander(mdmStorage, pusher)
hostUUIDs := []string{"A"}
payloadName := "com.foo.bar"
payloadIdentifier := "com-foo-bar"
mc := mobileconfigForTest(payloadName, payloadIdentifier)
mdmStorage.RetrievePushInfoFunc = func(p0 context.Context, targetUUIDs []string) (map[string]*mdm.Push, error) {
pushes := make(map[string]*mdm.Push, len(targetUUIDs))
for _, uuid := range targetUUIDs {
pushes[uuid] = &mdm.Push{
PushMagic: "magic" + uuid,
Token: []byte("token" + uuid),
Topic: "topic" + uuid,
}
}
return pushes, nil
}
mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
cert, err := tls.LoadX509KeyPair("../../service/testdata/server.pem", "../../service/testdata/server.key")
return &cert, "", err
}
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
return false, nil
}
mdmStorage.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
certPEM, err := os.ReadFile("../../service/testdata/server.pem")
require.NoError(t, err)
keyPEM, err := os.ReadFile("../../service/testdata/server.key")
require.NoError(t, err)
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Value: certPEM},
fleet.MDMAssetCAKey: {Value: keyPEM},
}, nil
}
t.Run("InstallProfile with a name", func(t *testing.T) {
var gotName string
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
gotName = cmd.Name
return nil, nil
}
mdmStorage.EnqueueCommandFuncInvoked = false
cmdUUID := uuid.New().String()
err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID, "My Profile")
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.Equal(t, "My Profile", gotName)
})
t.Run("InstallProfile without a name", func(t *testing.T) {
var gotName string
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
gotName = cmd.Name
return nil, nil
}
mdmStorage.EnqueueCommandFuncInvoked = false
cmdUUID := uuid.New().String()
err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID, "")
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.Empty(t, gotName)
})
t.Run("RemoveProfile with a name", func(t *testing.T) {
var gotName string
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
gotName = cmd.Name
return nil, nil
}
mdmStorage.EnqueueCommandFuncInvoked = false
cmdUUID := uuid.New().String()
err := cmdr.RemoveProfile(ctx, hostUUIDs, payloadIdentifier, cmdUUID, "My Profile")
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.Equal(t, "My Profile", gotName)
})
t.Run("EnqueueCommand no name", func(t *testing.T) {
var gotName string
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
gotName = cmd.Name
return nil, nil
}
mdmStorage.EnqueueCommandFuncInvoked = false
cmdUUID := uuid.New().String()
rawCmd := fmt.Sprintf(`<?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>CommandUUID</key>
<string>%s</string>
<key>Command</key>
<dict>
<key>RequestType</key>
<string>ProfileList</string>
</dict>
</dict>
</plist>`, cmdUUID)
err := cmdr.EnqueueCommand(ctx, hostUUIDs, rawCmd)
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.Empty(t, gotName)
})
t.Run("EnqueueCommandInstallProfileWithSecrets with a name", func(t *testing.T) {
var gotName string
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
gotName = cmd.Name
return nil, nil
}
mdmStorage.EnqueueCommandFuncInvoked = false
cmdUUID := uuid.New().String()
err := cmdr.EnqueueCommandInstallProfileWithSecrets(ctx, hostUUIDs, mc, cmdUUID, "Secret Profile")
require.NoError(t, err)
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
require.Equal(t, "Secret Profile", gotName)
})
}

View file

@ -97,14 +97,15 @@ func ProcessAndEnqueueProfiles(ctx context.Context,
switch op {
case fleet.MDMOperationTypeInstall:
if _, ok := profilesWithSecrets[profUUID]; ok {
err = commander.EnqueueCommandInstallProfileWithSecrets(ctx, target.EnrollmentIDs, profileContents[profUUID], target.CmdUUID)
err = commander.EnqueueCommandInstallProfileWithSecrets(ctx, target.EnrollmentIDs, profileContents[profUUID], target.CmdUUID, target.ProfileName)
} else {
err = commander.InstallProfile(ctx, target.EnrollmentIDs, profileContents[profUUID], target.CmdUUID)
err = commander.InstallProfile(ctx, target.EnrollmentIDs, profileContents[profUUID], target.CmdUUID, target.ProfileName)
}
case fleet.MDMOperationTypeRemove:
err = commander.RemoveProfile(ctx, target.EnrollmentIDs, target.ProfileIdentifier, target.CmdUUID)
err = commander.RemoveProfile(ctx, target.EnrollmentIDs, target.ProfileIdentifier, target.CmdUUID, target.ProfileName)
}
// Determine whether the command was enqueued (even if push notification failed).
var e *APNSDeliveryError
switch {
case errors.As(err, &e):
@ -117,6 +118,7 @@ func ProcessAndEnqueueProfiles(ctx context.Context,
default:
ch <- remoteResult{nil, target.CmdUUID}
}
}
for profUUID, target := range installTargets {
wgProd.Add(1)
@ -630,6 +632,7 @@ func preprocessProfileContents(
addedTargets[tempProfUUID] = &fleet.CmdTarget{
CmdUUID: tempCmdUUID,
ProfileIdentifier: target.ProfileIdentifier,
ProfileName: target.ProfileName,
EnrollmentIDs: []string{enrollmentID},
}
profileContents[tempProfUUID] = mobileconfig.Mobileconfig(hostContents)

View file

@ -62,6 +62,7 @@ type Command struct {
type CommandWithSubtype struct {
Command
Subtype CommandSubtype
Name string
}
// DecodeCommand unmarshals rawCommand into command

View file

@ -18,10 +18,18 @@ func enqueue(ctx context.Context, tx sqlx.ExtContext, ids []string, cmd *mdm.Com
if len(ids) < 1 {
return errors.New("no id(s) supplied to queue command to")
}
var nameArg sql.NullString
if cmd.Name != "" {
name := cmd.Name
if runes := []rune(name); len(runes) > 255 {
name = string(runes[:255])
}
nameArg = sql.NullString{String: name, Valid: true}
}
_, err := tx.ExecContext(
ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, subtype) VALUES (?, ?, ?, ?)`,
cmd.CommandUUID, cmd.Command.Command.RequestType, cmd.Raw, cmd.Subtype,
`INSERT INTO nano_commands (command_uuid, request_type, command, subtype, name) VALUES (?, ?, ?, ?, ?)`,
cmd.CommandUUID, cmd.Command.Command.RequestType, cmd.Raw, cmd.Subtype, nameArg,
)
if err != nil {
return err

View file

@ -144,6 +144,7 @@ CREATE TABLE nano_commands (
updated_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
subtype ENUM('None','ProfileWithSecrets') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'None',
name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (command_uuid),
@ -231,6 +232,7 @@ SELECT
c.command_uuid,
c.request_type,
c.command,
c.name,
r.updated_at AS result_updated_at,
r.status,
r.result

View file

@ -2330,7 +2330,7 @@ func (svc *Service) enqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co
}
cmdUUID := uuid.New().String()
err = svc.mdmAppleCommander.RemoveProfile(ctx, []string{nanoEnroll.ID}, apple_mdm.FleetPayloadIdentifier, cmdUUID)
err = svc.mdmAppleCommander.RemoveProfile(ctx, []string{nanoEnroll.ID}, apple_mdm.FleetPayloadIdentifier, cmdUUID, "")
if err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing mdm apple remove profile command")
}
@ -5262,6 +5262,7 @@ func ReconcileAppleProfiles(
target = &fleet.CmdTarget{
CmdUUID: uuid.New().String(),
ProfileIdentifier: p.ProfileIdentifier,
ProfileName: p.ProfileName,
}
installTargets[p.ProfileUUID] = target
}
@ -5365,6 +5366,7 @@ func ReconcileAppleProfiles(
target = &fleet.CmdTarget{
CmdUUID: uuid.New().String(),
ProfileIdentifier: p.ProfileIdentifier,
ProfileName: p.ProfileName,
}
removeTargets[p.ProfileUUID] = target
}
@ -5697,7 +5699,7 @@ func RenewSCEPCertificates(
if err != nil {
return ctxerr.Wrap(ctx, err, "generating enrollment profile for hosts without enroll reference")
}
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, filteredAssocs, profile); err != nil {
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, filteredAssocs, profile, appConfig.OrgInfo.OrgName+" enrollment"); err != nil {
return ctxerr.Wrap(ctx, err, "sending profile to hosts without associations")
}
}
@ -5738,7 +5740,7 @@ func RenewSCEPCertificates(
}
// each host with association needs a different enrollment profile, and thus a different command.
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile); err != nil {
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile, appConfig.OrgInfo.OrgName+" account driven enrollment"); err != nil {
return ctxerr.Wrap(ctx, err, "sending account driven enrollment profile renewal to hosts")
}
}
@ -5767,7 +5769,7 @@ func RenewSCEPCertificates(
}
// each host with association needs a different enrollment profile, and thus a different command.
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile); err != nil {
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile, appConfig.OrgInfo.OrgName+" enrollment"); err != nil {
return ctxerr.Wrap(ctx, err, "sending profile to hosts without associations")
}
}
@ -5805,7 +5807,7 @@ func RenewSCEPCertificates(
return ctxerr.Wrap(ctx, err, "generating enrollment profile for hosts requiring ACME renewal")
}
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile); err != nil {
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, []fleet.SCEPIdentityAssociation{assoc}, profile, appConfig.OrgInfo.OrgName+" ACME enrollment"); err != nil {
return ctxerr.Wrap(ctx, err, "sending ACME enrollment profile to hosts")
}
}
@ -5823,7 +5825,7 @@ func RenewSCEPCertificates(
}
if migrationEnrollmentProfile != "" && hasAssocsFromMigration {
profileBytes := []byte(migrationEnrollmentProfile)
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, assocsFromMigration, profileBytes); err != nil {
if err := renewMDMAppleEnrollmentProfile(ctx, ds, commander, logger, assocsFromMigration, profileBytes, appConfig.OrgInfo.OrgName+" migration enrollment"); err != nil {
return ctxerr.Wrap(ctx, err, "sending profile to hosts from migration")
}
}
@ -5838,6 +5840,7 @@ func renewMDMAppleEnrollmentProfile(
logger *slog.Logger,
assocs []fleet.SCEPIdentityAssociation,
profile []byte,
profileName string,
) error {
cmdUUID := uuid.NewString()
var uuids []string
@ -5857,7 +5860,7 @@ func renewMDMAppleEnrollmentProfile(
uuids = append(uuids, assoc.HostUUID)
}
if err := commander.InstallProfile(ctx, uuids, profile, cmdUUID); err != nil {
if err := commander.InstallProfile(ctx, uuids, profile, cmdUUID, profileName); err != nil {
return ctxerr.Wrapf(ctx, err, "sending InstallProfile command for hosts %s", uuids)
}

View file

@ -4638,6 +4638,7 @@ func TestRenewSCEPCertificatesBranches(t *testing.T) {
error,
) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t enrollment", cmd.Name)
wantCommandUUID = cmd.CommandUUID
return map[string]error{}, nil
}
@ -4681,6 +4682,7 @@ func TestRenewSCEPCertificatesBranches(t *testing.T) {
error,
) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t enrollment", cmd.Name)
wantCommandUUID = cmd.CommandUUID
return map[string]error{}, nil
}
@ -4740,6 +4742,7 @@ func TestRenewSCEPCertificatesBranches(t *testing.T) {
error,
) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t account driven enrollment", cmd.Name)
require.Equal(t, 1, len(id))
_, idAlreadyExists := wantCommandUUIDs[id[0]]
// Should only get one for each host
@ -4794,6 +4797,7 @@ func TestRenewSCEPCertificatesBranches(t *testing.T) {
error,
) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t account driven enrollment", cmd.Name)
require.Equal(t, 1, len(id))
_, idAlreadyExists := wantCommandUUIDs[id[0]]
// Should only get one for each host
@ -4985,6 +4989,7 @@ func TestRenewACMECertificatesBranches(t *testing.T) {
}}, nil
}
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
require.Equal(t, "fl33t enrollment", cmd.Name)
return map[string]error{}, nil
}
t.Cleanup(func() {
@ -5018,6 +5023,7 @@ func TestRenewACMECertificatesBranches(t *testing.T) {
}
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t ACME enrollment", cmd.Name)
wantCommandUUID = cmd.CommandUUID
// Verify the profile is an ACME profile by checking it contains the device serial
var fullCmd micromdm.CommandPayload
@ -5064,6 +5070,7 @@ func TestRenewACMECertificatesBranches(t *testing.T) {
}
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
require.Equal(t, "fl33t ACME enrollment", cmd.Name)
wantCommandUUID = cmd.CommandUUID
var fullCmd micromdm.CommandPayload
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
@ -5142,6 +5149,14 @@ func TestRenewACMECertificatesBranches(t *testing.T) {
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
require.Equal(t, "InstallProfile", cmd.Command.Command.RequestType)
for _, hostUUID := range id {
switch hostUUID {
case "hostUUID-acme":
require.Equal(t, "fl33t ACME enrollment", cmd.Name)
case "hostUUID-scep":
require.Equal(t, "fl33t enrollment", cmd.Name)
default:
require.Failf(t, "Unexpected host UUID", "Unexpected host UUID: %s", hostUUID)
}
enqueuedHostUUIDs[hostUUID] = true
}
return map[string]error{}, nil

View file

@ -725,6 +725,7 @@ func (a *AppleMDM) installProfilesForEnrollingHost(ctx context.Context, hostUUID
target := &fleet.CmdTarget{
CmdUUID: uuid.NewString(),
ProfileIdentifier: profile.ProfileIdentifier,
ProfileName: profile.ProfileName,
EnrollmentIDs: []string{hostUUID},
}
installTargets[profile.ProfileUUID] = target