07 — InstallApplication command shape for user-enrolled hosts
Goal
For user-enrolled iOS/iPadOS hosts, Fleet's InstallApplication MDM command must:
- Omit the
<key>ChangeManagementState</key><string>Managed</string> pair — Apple returns The MDM request is invalid. when this key is present on a user enrollment.
- Keep
ManagementFlags=1 (default; removes the app on enrollment removal — matches User Enrollment's data-separation semantics).
- Keep
InstallAsManaged=true and Options.PurchaseMethod=1.
- Route via the user channel if required by the enrollment (see subtask 06 step 4).
Applies to both VPP (iTunesStoreID) and in-house .ipa (ManifestURL) install paths.
Reference — current builders
Both are hardcoded XML strings in SQL:
- VPP:
server/datastore/mysql/vpp.go:2630–2679. ChangeManagementState at line 2658.
- In-house .ipa:
server/datastore/mysql/activities.go:1310–1407. ChangeManagementState at line 1345; ManagementFlags=1 for mobile at line 1403.
Marko's verified working XML for user enrollments (from the issue comment):
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>InstallAsManaged</key><true/>
<key>ManagementFlags</key><integer>1</integer>
<key>InstallAsManaged</key><true/>
<key>Options</key>
<dict><key>PurchaseMethod</key><integer>1</integer></dict>
<key>RequestType</key><string>InstallApplication</string>
<key>iTunesStoreID</key><integer>989804926</integer>
</dict>
<key>CommandUUID</key><string>...</string>
</dict>
</plist>
Work
1. Move command assembly out of inline SQL
Inline SQL string concatenation can't cleanly do per-row branching. Refactor both builders to assemble the plist in Go:
- New package/function, e.g.,
server/mdm/apple/installapp/build.go:
type InstallApplicationInput struct {
CommandUUID string
ITunesStoreID string // VPP
ManifestURL string // .ipa
ManagementFlags int
IsUserEnrollment bool // drives ChangeManagementState omission
}
func BuildInstallApplicationXML(in InstallApplicationInput) (string, error)
- VPP call site (
vpp.go:2630) and .ipa call site (activities.go:1310) both call the new builder and insert the resulting string into nano_commands.
2. User-enrollment flag
The builder's IsUserEnrollment comes from host_mdm.is_personal_enrollment, loaded at enqueue time along with the other host fields already fetched.
3. User-channel routing (if required)
If Apple requires the command on the user channel (not the device channel) for user enrollments, the nano_commands insert needs to target the user enrollment's command queue:
- Check nanomdm's user-channel API —
server/mdm/nanomdm/mdm/type.go:91 populates UserChannelID from EnrollmentUserID.
- Fleet's enqueue path (
InsertHostVPPSoftwareInstall + upcoming-activity mechanism) needs to know whether to enqueue on the device enrollment or the user enrollment.
- Easiest path: let nanomdm deliver via the user channel automatically when a user enrollment is present for that device. Verify empirically before writing fresh plumbing.
4. Tests
- Snapshot test on the XML output for:
- VPP + manually-enrolled → includes
ChangeManagementState
- VPP + user-enrolled → omits
ChangeManagementState
- .ipa + user-enrolled → omits
ChangeManagementState, has ManifestURL instead of iTunesStoreID
- Unit test on the builder directly.
Deliverables
- Refactored command builder in Go.
- Both VPP and .ipa enqueue paths route through it.
- Tests.
Acceptance
go test ./server/mdm/apple/... ./server/service/... ./server/datastore/mysql/... passes.
- Integration test (subtask 12) verifies the simulated device receives the correct XML per enrollment type.
Blockers
- Benefits from subtask 03 being in place (host → enrollment type lookup is easier with the new helper), but can be done without it.
07 — InstallApplication command shape for user-enrolled hosts
Goal
For user-enrolled iOS/iPadOS hosts, Fleet's
InstallApplicationMDM command must:<key>ChangeManagementState</key><string>Managed</string>pair — Apple returnsThe MDM request is invalid.when this key is present on a user enrollment.ManagementFlags=1(default; removes the app on enrollment removal — matches User Enrollment's data-separation semantics).InstallAsManaged=trueandOptions.PurchaseMethod=1.Applies to both VPP (
iTunesStoreID) and in-house.ipa(ManifestURL) install paths.Reference — current builders
Both are hardcoded XML strings in SQL:
server/datastore/mysql/vpp.go:2630–2679.ChangeManagementStateat line 2658.server/datastore/mysql/activities.go:1310–1407.ChangeManagementStateat line 1345;ManagementFlags=1for mobile at line 1403.Marko's verified working XML for user enrollments (from the issue comment):
Work
1. Move command assembly out of inline SQL
Inline SQL string concatenation can't cleanly do per-row branching. Refactor both builders to assemble the plist in Go:
server/mdm/apple/installapp/build.go:vpp.go:2630) and .ipa call site (activities.go:1310) both call the new builder and insert the resulting string intonano_commands.2. User-enrollment flag
The builder's
IsUserEnrollmentcomes fromhost_mdm.is_personal_enrollment, loaded at enqueue time along with the other host fields already fetched.3. User-channel routing (if required)
If Apple requires the command on the user channel (not the device channel) for user enrollments, the
nano_commandsinsert needs to target the user enrollment's command queue:server/mdm/nanomdm/mdm/type.go:91populatesUserChannelIDfromEnrollmentUserID.InsertHostVPPSoftwareInstall+ upcoming-activity mechanism) needs to know whether to enqueue on the device enrollment or the user enrollment.4. Tests
ChangeManagementStateChangeManagementStateChangeManagementState, hasManifestURLinstead ofiTunesStoreIDDeliverables
Acceptance
go test ./server/mdm/apple/... ./server/service/... ./server/datastore/mysql/...passes.Blockers