Skip to content

Commit 06d7169

Browse files
authored
Install setup-experience VPP apps on manually-enrolled iOS/iPadOS devices (#35906)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34042 # 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. ## Testing - [X] Added/updated automated tests - [X] QA'd all new/changed functionality manually Tested on iPad and iOS. Full disclosure, VPP installs on my devices seemed to sometimes (not not always) fail silently the first time I tried them, with no `error` showing in the setup experience results. This could be due to the vagaries of user-based vpp licensing vs. device-based, which is perhaps not a real-world situation, or something else I'm not following with [how VPP license assignments work](https://github.com/fleetdm/fleet/blob/10889199a1754011119bb19df022b545e352c77d/ee/server/service/software_installers.go#L1299-L1310). I'll continue trying to reproduce it but it's difficult since it only seems to happen once per app at most, and I can't remove the user licenses from a device without wiping it (I don't have any physical devices I can do this on).
1 parent b5286a3 commit 06d7169

File tree

4 files changed

+195
-10
lines changed

4 files changed

+195
-10
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Support installation of setup-experience VPP apps on manual-enrolled iOS/iPadOS devices

server/service/apple_mdm.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3518,17 +3518,33 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm.
35183518
}
35193519

35203520
var hasSetupExpItems bool
3521+
enqueueSetupExperienceItems := false
3522+
35213523
if m.AwaitingConfiguration {
3522-
// Always run setup experience on non-macOS hosts(i.e. iOS/iPadOS), only run it on macOS if
3523-
// this is not an ABM MDM migration
3524-
if info.Platform != "darwin" || !info.MigrationInProgress {
3525-
// Enqueue setup experience items and mark the host as being in setup experience
3526-
hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, info.Platform, r.ID, info.TeamID)
3527-
if err != nil {
3528-
return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks")
3529-
}
3530-
} else {
3524+
if info.MigrationInProgress {
35313525
svc.logger.Log("info", "skipping setup experience enqueueing because DEP migration is in progress", "host_uuid", r.ID)
3526+
} else {
3527+
enqueueSetupExperienceItems = true
3528+
}
3529+
} else if info.Platform != "darwin" && r.Type == mdm.Device && !info.InstalledFromDEP {
3530+
// For manual iOS/iPadOS device enrollments, check the `TokenUpdateTally` so that
3531+
// we only run the setup experience enqueueing once per device.
3532+
nanoEnroll, err := svc.ds.GetNanoMDMEnrollment(r.Context, r.ID)
3533+
if err != nil {
3534+
return ctxerr.Wrap(r.Context, err, "getting nanomdm enrollment")
3535+
}
3536+
if nanoEnroll != nil && nanoEnroll.TokenUpdateTally == 1 {
3537+
enqueueSetupExperienceItems = true
3538+
}
3539+
}
3540+
3541+
// TODO -- See if there's a way to check license here to avoid unnecessary work.
3542+
// We do check the license before actually _running_ setup experience items.
3543+
if enqueueSetupExperienceItems {
3544+
// Enqueue setup experience items and mark the host as being in setup experience
3545+
hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, info.Platform, r.ID, info.TeamID)
3546+
if err != nil {
3547+
return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks")
35323548
}
35333549
}
35343550

server/service/apple_mdm_test.go

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1437,7 +1437,7 @@ func TestMDMUnenrollment(t *testing.T) {
14371437
}
14381438

14391439
func TestMDMTokenUpdate(t *testing.T) {
1440-
ctx := context.Background()
1440+
ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
14411441
ds := new(mock.Store)
14421442
mdmStorage := &mdmmock.MDMAppleStore{}
14431443
pushFactory, _ := newMockAPNSPushProviderFactory()
@@ -1600,6 +1600,164 @@ func TestMDMTokenUpdate(t *testing.T) {
16001600
require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked)
16011601
}
16021602

1603+
func TestMDMTokenUpdateIOS(t *testing.T) {
1604+
ctx := context.Background()
1605+
ds := new(mock.Store)
1606+
mdmStorage := &mdmmock.MDMAppleStore{}
1607+
pushFactory, _ := newMockAPNSPushProviderFactory()
1608+
pusher := nanomdm_pushsvc.New(
1609+
mdmStorage,
1610+
mdmStorage,
1611+
pushFactory,
1612+
NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)),
1613+
)
1614+
cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
1615+
mdmLifecycle := mdmlifecycle.New(ds, kitlog.NewNopLogger(), newActivity)
1616+
svc := MDMAppleCheckinAndCommandService{
1617+
ds: ds,
1618+
mdmLifecycle: mdmLifecycle,
1619+
commander: cmdr,
1620+
logger: kitlog.NewNopLogger(),
1621+
}
1622+
uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12)
1623+
1624+
ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) {
1625+
require.Equal(t, uuid, hostUUID)
1626+
return &fleet.MDMIdPAccount{
1627+
UUID: "some-uuid",
1628+
Username: "some-user",
1629+
1630+
Fullname: "Some User",
1631+
}, nil
1632+
}
1633+
1634+
ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) {
1635+
return j, nil
1636+
}
1637+
1638+
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
1639+
require.Equal(t, uuid, hostUUID)
1640+
return &fleet.HostMDMCheckinInfo{
1641+
HostID: 1337,
1642+
HardwareSerial: serial,
1643+
DisplayName: model,
1644+
InstalledFromDEP: true,
1645+
TeamID: wantTeamID,
1646+
DEPAssignedToFleet: true,
1647+
Platform: "ios",
1648+
}, nil
1649+
}
1650+
1651+
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
1652+
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
1653+
}
1654+
1655+
ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) {
1656+
require.Equal(t, "ios", hostPlatformLike)
1657+
require.Equal(t, uuid, hostUUID)
1658+
require.Equal(t, wantTeamID, teamID)
1659+
return true, nil
1660+
}
1661+
1662+
// DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items
1663+
err := svc.TokenUpdate(
1664+
&mdm.Request{
1665+
Context: ctx,
1666+
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
1667+
Params: map[string]string{"enroll_reference": "abcd"},
1668+
},
1669+
&mdm.TokenUpdate{
1670+
Enrollment: mdm.Enrollment{
1671+
UDID: uuid,
1672+
},
1673+
},
1674+
)
1675+
require.NoError(t, err)
1676+
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
1677+
1678+
// Non-DEP-installed, non device-type enrollment should not enqueue SetupExperience items
1679+
err = svc.TokenUpdate(
1680+
&mdm.Request{
1681+
Context: ctx,
1682+
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.User},
1683+
Params: map[string]string{"enroll_reference": "abcd"},
1684+
},
1685+
&mdm.TokenUpdate{
1686+
Enrollment: mdm.Enrollment{
1687+
UDID: uuid,
1688+
},
1689+
},
1690+
)
1691+
require.NoError(t, err)
1692+
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
1693+
1694+
// Non-DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items if token count is > 1
1695+
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
1696+
require.Equal(t, uuid, hostUUID)
1697+
return &fleet.HostMDMCheckinInfo{
1698+
HostID: 1337,
1699+
HardwareSerial: serial,
1700+
DisplayName: model,
1701+
InstalledFromDEP: false,
1702+
TeamID: wantTeamID,
1703+
DEPAssignedToFleet: true,
1704+
Platform: "ios",
1705+
}, nil
1706+
}
1707+
1708+
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
1709+
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 2}, nil
1710+
}
1711+
1712+
err = svc.TokenUpdate(
1713+
&mdm.Request{
1714+
Context: ctx,
1715+
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
1716+
Params: map[string]string{"enroll_reference": "abcd"},
1717+
},
1718+
&mdm.TokenUpdate{
1719+
Enrollment: mdm.Enrollment{
1720+
UDID: uuid,
1721+
},
1722+
},
1723+
)
1724+
require.NoError(t, err)
1725+
require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
1726+
1727+
// Non-DEP-installed without AwaitingConfiguration - should enqueue SetupExperience items if token count is 1
1728+
ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
1729+
require.Equal(t, uuid, hostUUID)
1730+
return &fleet.HostMDMCheckinInfo{
1731+
HostID: 1337,
1732+
HardwareSerial: serial,
1733+
DisplayName: model,
1734+
InstalledFromDEP: false,
1735+
TeamID: wantTeamID,
1736+
DEPAssignedToFleet: true,
1737+
Platform: "ios",
1738+
}, nil
1739+
}
1740+
1741+
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
1742+
return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil
1743+
}
1744+
1745+
err = svc.TokenUpdate(
1746+
&mdm.Request{
1747+
Context: ctx,
1748+
EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device},
1749+
Params: map[string]string{"enroll_reference": "abcd"},
1750+
},
1751+
&mdm.TokenUpdate{
1752+
Enrollment: mdm.Enrollment{
1753+
UDID: uuid,
1754+
},
1755+
},
1756+
)
1757+
require.NoError(t, err)
1758+
require.True(t, ds.EnqueueSetupExperienceItemsFuncInvoked)
1759+
}
1760+
16031761
func TestMDMCheckout(t *testing.T) {
16041762
ds := new(mock.Store)
16051763
mdmLifecycle := mdmlifecycle.New(ds, kitlog.NewNopLogger(), newActivity)

server/worker/apple_mdm.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
1313
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
14+
"github.com/fleetdm/fleet/v4/server/contexts/license"
1415
"github.com/fleetdm/fleet/v4/server/fleet"
1516
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
1617
"github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest"
@@ -114,6 +115,15 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg
114115
if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
115116
return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
116117
}
118+
} else {
119+
// We shouldn't have any setup experience steps if we're not on a premium license,
120+
// but best to check anyway plus it saves some db queries.
121+
if license.IsPremium(ctx) {
122+
_, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
123+
if err != nil {
124+
return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
125+
}
126+
}
117127
}
118128

119129
return nil

0 commit comments

Comments
 (0)