**Related issue:** Resolves#48879
# Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (parameterized queries only).
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes — N/A,
no endpoint/path changes.
## Summary
Installing an App Store (VPP) or in-house app on an iOS/iPadOS host
enrolled via the **manual (profile-driven) BYOD** enrollment profile
failed: Fleet routed the install down the **Account-Driven User
Enrollment (user-scoped)** licensing path, tried to look up/register a
VPP user keyed on the host's Managed Apple ID, and returned _"Fleet
hasn't received a Managed Apple ID for this host yet."_ — which never
resolves, because a device-channel host has no Managed Apple ID.
### Root cause
The device-vs-user licensing decision keyed off
`host_mdm.is_personal_enrollment`. That flag is set for **both**:
- **Account-Driven User Enrollment** — user channel, backed by a Managed
Apple ID → user-scoped licensing (correct).
- **Manual-profile BYOD** — device channel, no Managed Apple ID → must
install **device-scoped**, exactly like company-owned manual enrollment.
### Fix
Branch on the actual enrollment **channel** — the presence of a
user-channel `nano_enrollments` row (`type='User' AND enabled=1`), the
same signal the MDM profile reconcile cron already uses
(`GetNanoMDMUserEnrollment`). This is timing-robust: the user
nano-enrollment exists from enrollment time, whereas the Managed Apple
ID only arrives minutes later via `TokenUpdate` (so `managed_apple_id`
emptiness is deliberately **not** used as the discriminator).
Three sites updated:
| File | Change |
|---|---|
| `ee/server/service/software_installers.go` |
`InstallVPPAppPostValidation` routes on `GetNanoMDMUserEnrollment`
instead of `is_personal_enrollment` |
| `server/datastore/mysql/vpp.go` | InstallApplication builder derives
`IsUserEnrollment` (ChangeManagementState omission) from a user-channel
`nano_enrollments` row |
| `server/datastore/mysql/activities.go` | same, for in-house `.ipa`
installs |
## Testing
- [x] Added/updated automated tests:
- `ee/server/service`:
`TestInstallVPPAppPostValidation_AssociateAssetsRouting` — added a
regression subtest asserting manual-profile BYOD (personal flag set,
device channel) routes via `serialNumbers` and performs **no** VPP user
lookup; repointed routing to the user-channel signal.
- `server/datastore/mysql`: new
`TestVPP/VPPInstallEnrollmentChannelRouting` — manual BYOD includes
`ChangeManagementState` despite `is_personal_enrollment=1`;
account-driven User Enrollment omits it.
- [x] Automated tests simulate multiple hosts and test for host
isolation (two distinct hosts, device- vs user-channel).
- [ ] QA'd all new/changed functionality manually — pending (draft).
For unreleased bug fixes in a release candidate:
- [x] Confirmed that the fix is not expected to adversely impact load
test results (adds one indexed lookup per install enqueue; removes a
`host_mdm` join).
## Database migrations
- N/A — no schema changes. The fix reads existing `nano_enrollments`
rows.
## fleetd/orbit/Fleet Desktop
- N/A
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed app installation for manually enrolled BYOD iPhone and iPad
devices so App Store and in-house apps install correctly on the device.
* Improved enrollment handling so device-scoped installs no longer fail
when a device is marked personal in one place but uses device-channel
enrollment.
* Account-Driven User Enrollment continues to use user-scoped licensing
and installs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->