**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 -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Fixed a bug where installing App Store (VPP) or in-house apps on an iOS/iPadOS host enrolled with the manual (profile-driven) BYOD enrollment profile failed while trying to look up a VPP user. These device-channel hosts now install apps to the device, the same as company-owned manual enrollment; user-scoped licensing is reserved for Account-Driven User Enrollment.
|
||||
@@ -77,13 +77,24 @@ func TestInstallVPPAppPostValidation_AssociateAssetsRouting(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Common datastore mock setup, parameterized by isPersonalEnrollment.
|
||||
setupDS := func(t *testing.T, isPersonal bool) *mock.Store {
|
||||
// Common datastore mock setup, parameterized by whether the host is enrolled
|
||||
// via Account-Driven User Enrollment (ADUE). The routing decision keys off
|
||||
// the host's primary nano_enrollments row (id = host UUID): ADUE devices
|
||||
// enroll as "User Enrollment (Device)", every other device-channel
|
||||
// enrollment (including manual-profile BYOD) is "Device". It does NOT key
|
||||
// off host_mdm.is_personal_enrollment (see #48879), so this drives
|
||||
// GetNanoMDMEnrollment rather than GetHostMDM.
|
||||
setupDS := func(t *testing.T, isUserEnrollment bool) *mock.Store {
|
||||
t.Helper()
|
||||
ds := new(mock.Store)
|
||||
ds.GetHostMDMFunc = func(_ context.Context, id uint) (*fleet.HostMDM, error) {
|
||||
require.Equal(t, hostID, id)
|
||||
return &fleet.HostMDM{HostID: id, Enrolled: true, IsPersonalEnrollment: isPersonal}, nil
|
||||
ds.GetNanoMDMEnrollmentFunc = func(_ context.Context, id string) (*fleet.NanoEnrollment, error) {
|
||||
require.Equal(t, hostUUID, id)
|
||||
if isUserEnrollment {
|
||||
return &fleet.NanoEnrollment{ID: hostUUID, DeviceID: hostUUID, Type: "User Enrollment (Device)", Enabled: true}, nil
|
||||
}
|
||||
// Device-channel enrollment (company-owned manual OR manual-profile
|
||||
// BYOD): the primary row is type "Device".
|
||||
return &fleet.NanoEnrollment{ID: hostUUID, DeviceID: hostUUID, Type: "Device", Enabled: true}, nil
|
||||
}
|
||||
ds.GetVPPTokenByTeamIDFunc = func(_ context.Context, _ *uint) (*fleet.VPPTokenDB, error) {
|
||||
return &fleet.VPPTokenDB{ID: 99, Token: bearerToken, RenewDate: time.Now().Add(24 * time.Hour)}, nil
|
||||
@@ -136,7 +147,7 @@ func TestInstallVPPAppPostValidation_AssociateAssetsRouting(t *testing.T) {
|
||||
require.True(t, ds.InsertVPPClientUserFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("non-personal enrollment keeps SerialNumbers", func(t *testing.T) {
|
||||
t.Run("device-channel enrollment keeps SerialNumbers", func(t *testing.T) {
|
||||
var capt captured
|
||||
setupServer(t, &capt)
|
||||
|
||||
@@ -152,15 +163,53 @@ func TestInstallVPPAppPostValidation_AssociateAssetsRouting(t *testing.T) {
|
||||
SerialNumbers []string `json:"serialNumbers"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(capt.body, &got))
|
||||
require.Equal(t, []string{hostSerial}, got.SerialNumbers, "manually-enrolled hosts must use serialNumbers")
|
||||
require.Empty(t, got.ClientUserIds, "manually-enrolled hosts must not send clientUserIds")
|
||||
require.Equal(t, []string{hostSerial}, got.SerialNumbers, "device-channel hosts must use serialNumbers")
|
||||
require.Empty(t, got.ClientUserIds, "device-channel hosts must not send clientUserIds")
|
||||
|
||||
// User-provisioning datastore writes must NOT happen on the manual path.
|
||||
// User-provisioning datastore writes must NOT happen on the device path.
|
||||
require.False(t, ds.GetVPPClientUserFuncInvoked)
|
||||
require.False(t, ds.InsertVPPClientUserFuncInvoked)
|
||||
require.False(t, ds.GetHostManagedAppleIDFuncInvoked)
|
||||
})
|
||||
|
||||
// Regression test for #48879: a manual-profile BYOD host carries
|
||||
// host_mdm.is_personal_enrollment=1 but is a DEVICE-channel enrollment (its
|
||||
// primary nano_enrollments row is type "Device", not "User Enrollment
|
||||
// (Device)", and it has no Managed Apple ID). It must install device-scoped
|
||||
// (serialNumbers), exactly like company-owned manual — and must NOT attempt
|
||||
// user provisioning, which previously failed with errMissingManagedAppleID.
|
||||
t.Run("manual-profile BYOD (personal flag, device channel) routes via serialNumbers", func(t *testing.T) {
|
||||
var capt captured
|
||||
setupServer(t, &capt)
|
||||
|
||||
// isUserEnrollment=false → the primary enrollment row is type "Device"
|
||||
// even though this host would have is_personal_enrollment=1 in host_mdm.
|
||||
ds := setupDS(t, false)
|
||||
// Make it explicit that the Managed Apple ID is absent for this host, so a
|
||||
// regression that re-introduces the user path would fail loudly here.
|
||||
ds.GetHostManagedAppleIDFunc = func(_ context.Context, _ uint) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
||||
|
||||
_, err := svc.InstallVPPAppPostValidation(context.Background(), host, vppApp, bearerToken, fleet.HostSoftwareInstallOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEmpty(t, capt.body)
|
||||
var got struct {
|
||||
ClientUserIds []string `json:"clientUserIds"`
|
||||
SerialNumbers []string `json:"serialNumbers"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(capt.body, &got))
|
||||
require.Equal(t, []string{hostSerial}, got.SerialNumbers, "manual-profile BYOD must use serialNumbers (device-scoped)")
|
||||
require.Empty(t, got.ClientUserIds, "manual-profile BYOD must not send clientUserIds")
|
||||
|
||||
// The whole point of #48879: no VPP user lookup/registration for device-channel BYOD.
|
||||
require.False(t, ds.GetHostManagedAppleIDFuncInvoked, "must not look up a Managed Apple ID for device-channel BYOD")
|
||||
require.False(t, ds.GetVPPClientUserFuncInvoked)
|
||||
require.False(t, ds.InsertVPPClientUserFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("personal enrollment queries assignments by clientUserId", func(t *testing.T) {
|
||||
var assignmentsQuery string
|
||||
setupFakeVPPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps"
|
||||
nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
@@ -1657,14 +1658,28 @@ func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet
|
||||
// makes Fleet enter the AvailableCount check on every retry and produces
|
||||
// false-positive "no available licenses" errors when the user is just
|
||||
// adding their Nth (≤5) device under one Managed Apple ID.
|
||||
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||
// Device-vs-user VPP licensing must key off the actual MDM enrollment
|
||||
// channel, not host_mdm.is_personal_enrollment. is_personal_enrollment is
|
||||
// set for BOTH Account-Driven User Enrollment (ADUE, user-scoped, backed by
|
||||
// a Managed Apple ID) and manual-profile BYOD (device channel, no Managed
|
||||
// Apple ID). Only ADUE gets user-scoped licensing; manual-profile BYOD
|
||||
// installs device-scoped, exactly like company-owned manual enrollment.
|
||||
//
|
||||
// The host's primary nano_enrollments row (id = host UUID) tells us the
|
||||
// channel: ADUE devices enroll as "User Enrollment (Device)", while every
|
||||
// other device-channel enrollment (ADE, manual, manual-profile BYOD) is
|
||||
// "Device". Note the "User" type is the separate macOS user channel and is
|
||||
// NOT what we want here. This row exists from enrollment time, whereas the
|
||||
// Managed Apple ID only arrives minutes later via TokenUpdate, so this is
|
||||
// the correct, timing-robust signal. See #48879.
|
||||
nanoEnroll, err := svc.ds.GetNanoMDMEnrollment(ctx, host.UUID)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "looking up host MDM info for VPP install")
|
||||
return "", ctxerr.Wrap(ctx, err, "looking up enrollment for VPP install")
|
||||
}
|
||||
isPersonal := hostMDM != nil && hostMDM.IsPersonalEnrollment
|
||||
isUserEnrollment := nanoEnroll != nil && nanoEnroll.Type == nanomdm.EnrollType(nanomdm.UserEnrollmentDevice).String()
|
||||
|
||||
var clientUserID string
|
||||
if isPersonal {
|
||||
if isUserEnrollment {
|
||||
// Token-selection policy (per #44009): use the team's default token —
|
||||
// `GetVPPTokenByTeamID` already returns the first token for the team
|
||||
// (existing behavior). Multi-location support is deferred unless a
|
||||
@@ -1680,7 +1695,7 @@ func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet
|
||||
}
|
||||
|
||||
assignmentFilter := &vpp.AssignmentFilter{AdamID: vppApp.AdamID}
|
||||
if isPersonal {
|
||||
if isUserEnrollment {
|
||||
assignmentFilter.ClientUserID = clientUserID
|
||||
} else {
|
||||
assignmentFilter.SerialNumber = host.HardwareSerial
|
||||
@@ -1735,7 +1750,7 @@ func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet
|
||||
}
|
||||
|
||||
req := &vpp.AssociateAssetsRequest{Assets: assets}
|
||||
if isPersonal {
|
||||
if isUserEnrollment {
|
||||
req.ClientUserIds = []string{clientUserID}
|
||||
} else {
|
||||
req.SerialNumbers = []string{host.HardwareSerial}
|
||||
|
||||
@@ -1402,16 +1402,26 @@ ORDER BY
|
||||
ua.priority DESC, ua.created_at ASC
|
||||
`
|
||||
|
||||
// is_user_enrollment must reflect the actual MDM enrollment channel, NOT
|
||||
// host_mdm.is_personal_enrollment: the latter is also set for
|
||||
// manual-profile BYOD, which is device-channel and must install
|
||||
// device-scoped like company-owned manual. Only Account-Driven User
|
||||
// Enrollment (ADUE) is user-scoped, and its primary enrollment row
|
||||
// (id = host UUID) has type 'User Enrollment (Device)' — every other
|
||||
// device-channel enrollment is 'Device'. See #48879.
|
||||
const getHostStmt = `
|
||||
SELECT
|
||||
h.uuid,
|
||||
h.team_id,
|
||||
h.platform,
|
||||
h.hardware_serial,
|
||||
COALESCE(hm.is_personal_enrollment, 0) AS is_personal_enrollment
|
||||
COALESCE((
|
||||
SELECT 1 FROM nano_enrollments ne
|
||||
WHERE ne.id = h.uuid AND ne.type = 'User Enrollment (Device)' AND ne.enabled = 1
|
||||
LIMIT 1
|
||||
), 0) AS is_user_enrollment
|
||||
FROM
|
||||
hosts h
|
||||
LEFT JOIN host_mdm hm ON hm.host_id = h.id
|
||||
WHERE
|
||||
h.id = ?
|
||||
`
|
||||
@@ -1439,11 +1449,11 @@ ORDER BY
|
||||
}
|
||||
|
||||
var hostData struct {
|
||||
UUID string `db:"uuid"`
|
||||
TeamID *uint `db:"team_id"`
|
||||
Platform string `db:"platform"`
|
||||
HardwareSerial string `db:"hardware_serial"`
|
||||
IsPersonalEnrollment bool `db:"is_personal_enrollment"`
|
||||
UUID string `db:"uuid"`
|
||||
TeamID *uint `db:"team_id"`
|
||||
Platform string `db:"platform"`
|
||||
HardwareSerial string `db:"hardware_serial"`
|
||||
IsUserEnrollment bool `db:"is_user_enrollment"`
|
||||
}
|
||||
if err := sqlx.GetContext(ctx, tx, &hostData, getHostStmt, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host info for in-house install")
|
||||
@@ -1546,7 +1556,7 @@ WHERE
|
||||
HostPlatform: hostData.Platform,
|
||||
ManifestURL: manifestURL,
|
||||
Configuration: cfg,
|
||||
IsUserEnrollment: hostData.IsPersonalEnrollment,
|
||||
IsUserEnrollment: hostData.IsUserEnrollment,
|
||||
})
|
||||
insValues = append(insValues, "(?, 'InstallApplication', ?, ?)")
|
||||
insArgs = append(insArgs, p.ExecutionID, string(cmdBytes), mdm.CommandSubtypeNone)
|
||||
|
||||
@@ -3107,25 +3107,35 @@ func (ds *Datastore) nanoEnqueueVPPInstall(ctx context.Context, tx sqlx.ExtConte
|
||||
return nil
|
||||
}
|
||||
|
||||
// is_user_enrollment must reflect the actual MDM enrollment channel, NOT
|
||||
// host_mdm.is_personal_enrollment: the latter is also set for
|
||||
// manual-profile BYOD, which is device-channel and must install
|
||||
// device-scoped like company-owned manual. Only Account-Driven User
|
||||
// Enrollment (ADUE) is user-scoped, and its primary enrollment row
|
||||
// (id = host UUID) has type 'User Enrollment (Device)' — every other
|
||||
// device-channel enrollment is 'Device'. See #48879.
|
||||
const getHostUUIDStmt = `
|
||||
SELECT
|
||||
h.uuid,
|
||||
h.platform,
|
||||
h.team_id,
|
||||
h.hardware_serial,
|
||||
COALESCE(hm.is_personal_enrollment, 0) AS is_personal_enrollment
|
||||
COALESCE((
|
||||
SELECT 1 FROM nano_enrollments ne
|
||||
WHERE ne.id = h.uuid AND ne.type = 'User Enrollment (Device)' AND ne.enabled = 1
|
||||
LIMIT 1
|
||||
), 0) AS is_user_enrollment
|
||||
FROM
|
||||
hosts h
|
||||
LEFT JOIN host_mdm hm ON hm.host_id = h.id
|
||||
WHERE
|
||||
h.id = ?
|
||||
`
|
||||
var hostData struct {
|
||||
UUID string `db:"uuid"`
|
||||
Platform string `db:"platform"`
|
||||
TeamID *uint `db:"team_id"`
|
||||
HardwareSerial string `db:"hardware_serial"`
|
||||
IsPersonalEnrollment bool `db:"is_personal_enrollment"`
|
||||
UUID string `db:"uuid"`
|
||||
Platform string `db:"platform"`
|
||||
TeamID *uint `db:"team_id"`
|
||||
HardwareSerial string `db:"hardware_serial"`
|
||||
IsUserEnrollment bool `db:"is_user_enrollment"`
|
||||
}
|
||||
if err := sqlx.GetContext(ctx, tx, &hostData, getHostUUIDStmt, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host info for vpp install")
|
||||
@@ -3216,7 +3226,7 @@ WHERE
|
||||
HostPlatform: hostData.Platform,
|
||||
ITunesStoreID: p.AdamID,
|
||||
Configuration: cfg,
|
||||
IsUserEnrollment: hostData.IsPersonalEnrollment,
|
||||
IsUserEnrollment: hostData.IsUserEnrollment,
|
||||
})
|
||||
insValues = append(insValues, "(?, 'InstallApplication', ?, ?)")
|
||||
insArgs = append(insArgs, p.ExecutionID, string(cmdBytes), mdm.CommandSubtypeNone)
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestVPP(t *testing.T) {
|
||||
{"VPPAppConfigDeletedOnTeamDelete", testVPPAppConfigDeletedOnTeamDelete},
|
||||
{"VPPInstallEnqueuesConfigurationDict", testVPPInstallEnqueuesConfigurationDict},
|
||||
{"VPPInstallOmitsConfigurationOnMacOS", testVPPInstallOmitsConfigurationOnMacOS},
|
||||
{"VPPInstallEnrollmentChannelRouting", testVPPInstallEnrollmentChannelRouting},
|
||||
{"MapAdamIDsPendingInstallVerification", testMapAdamIDsPendingInstallVerification},
|
||||
{"MapAdamIDsRecentInstalls", testMapAdamIDsRecentInstalls},
|
||||
{"MapAdamIDsRecentlyVerifiedInstalls", testMapAdamIDsRecentlyVerifiedInstalls},
|
||||
@@ -3444,6 +3445,75 @@ func testVPPInstallOmitsConfigurationOnMacOS(t *testing.T, ds *Datastore) {
|
||||
require.Contains(t, commandXML, "<key>iTunesStoreID</key>")
|
||||
}
|
||||
|
||||
// testVPPInstallEnrollmentChannelRouting is the #48879 regression at the
|
||||
// enqueue layer. The InstallApplication command's IsUserEnrollment flag (which
|
||||
// omits ChangeManagementState, valid only on Apple's Account-Driven User
|
||||
// Enrollment channel) must be driven by the actual enrollment CHANNEL — the
|
||||
// host's primary nano_enrollments row (id = host UUID) being type
|
||||
// "User Enrollment (Device)" — NOT by host_mdm.is_personal_enrollment. A
|
||||
// manual-profile BYOD host has is_personal_enrollment=1 but is device-channel
|
||||
// (primary row type "Device"), so its command must include ChangeManagementState
|
||||
// exactly like company-owned manual.
|
||||
func testVPPInstallEnrollmentChannelRouting(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
|
||||
const adamID = "77778888"
|
||||
setupTestVPPApp(t, ds, adamID, fleet.IOSPlatform)
|
||||
vppApp := &fleet.VPPApp{
|
||||
Name: "ChannelApp",
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: adamID, Platform: fleet.IOSPlatform}},
|
||||
BundleIdentifier: adamID,
|
||||
}
|
||||
_, err := ds.InsertVPPAppWithTeam(ctx, vppApp, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
enqueueAndReadCommand := func(t *testing.T, host *fleet.Host, cmdUUID string) string {
|
||||
require.NoError(t, ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, "evt-"+cmdUUID, fleet.HostSoftwareInstallOptions{}))
|
||||
var commandXML string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &commandXML, "SELECT command FROM nano_commands WHERE command_uuid = ?", cmdUUID)
|
||||
})
|
||||
return commandXML
|
||||
}
|
||||
|
||||
t.Run("manual-profile BYOD (personal flag, device channel) includes ChangeManagementState", func(t *testing.T) {
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "byod-manual-ios",
|
||||
UUID: "byod-manual-uuid",
|
||||
Platform: string(fleet.IOSPlatform),
|
||||
HardwareSerial: "BYOD-SERIAL",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Device-channel enrollment (primary row type "Device") ...
|
||||
nanoEnroll(t, ds, host, false)
|
||||
// ... but flagged personal in host_mdm, like a manual BYOD profile.
|
||||
require.NoError(t, ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://fleetdm.com", false, fleet.WellKnownMDMFleet, "", true))
|
||||
|
||||
commandXML := enqueueAndReadCommand(t, host, "byod-manual-cmd")
|
||||
require.Contains(t, commandXML, "<key>ChangeManagementState</key>",
|
||||
"manual-profile BYOD is device-channel and must include ChangeManagementState despite is_personal_enrollment=1 (#48879)")
|
||||
})
|
||||
|
||||
t.Run("account-driven user enrollment omits ChangeManagementState", func(t *testing.T) {
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "adue-ios",
|
||||
UUID: "adue-uuid",
|
||||
Platform: string(fleet.IOSPlatform),
|
||||
HardwareSerial: "ADUE-SERIAL",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Account-Driven User Enrollment: the primary enrollment row (id = host
|
||||
// UUID) is type "User Enrollment (Device)".
|
||||
nanoEnrollUserDevice(t, ds, host)
|
||||
require.NoError(t, ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://fleetdm.com", false, fleet.WellKnownMDMFleet, "", true))
|
||||
|
||||
commandXML := enqueueAndReadCommand(t, host, "adue-cmd")
|
||||
require.NotContains(t, commandXML, "<key>ChangeManagementState</key>",
|
||||
"account-driven user enrollment must omit ChangeManagementState")
|
||||
})
|
||||
}
|
||||
|
||||
func testHasVPPAppConfigurationChanged(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
const adamID = "1234567890"
|
||||
|
||||
Reference in New Issue
Block a user