diff --git a/changes/34042-setup-experience-on-manual-ios b/changes/34042-setup-experience-on-manual-ios new file mode 100644 index 0000000000..507c65780b --- /dev/null +++ b/changes/34042-setup-experience-on-manual-ios @@ -0,0 +1 @@ +- Support installation of setup-experience VPP apps on manual-enrolled iOS/iPadOS devices diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 8dd2aafd56..12f23f314c 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -3518,17 +3518,33 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. } var hasSetupExpItems bool + enqueueSetupExperienceItems := false + if m.AwaitingConfiguration { - // Always run setup experience on non-macOS hosts(i.e. iOS/iPadOS), only run it on macOS if - // this is not an ABM MDM migration - if info.Platform != "darwin" || !info.MigrationInProgress { - // Enqueue setup experience items and mark the host as being in setup experience - hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, info.Platform, r.ID, info.TeamID) - if err != nil { - return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks") - } - } else { + if info.MigrationInProgress { svc.logger.Log("info", "skipping setup experience enqueueing because DEP migration is in progress", "host_uuid", r.ID) + } else { + enqueueSetupExperienceItems = true + } + } else if info.Platform != "darwin" && r.Type == mdm.Device && !info.InstalledFromDEP { + // For manual iOS/iPadOS device enrollments, check the `TokenUpdateTally` so that + // we only run the setup experience enqueueing once per device. + nanoEnroll, err := svc.ds.GetNanoMDMEnrollment(r.Context, r.ID) + if err != nil { + return ctxerr.Wrap(r.Context, err, "getting nanomdm enrollment") + } + if nanoEnroll != nil && nanoEnroll.TokenUpdateTally == 1 { + enqueueSetupExperienceItems = true + } + } + + // TODO -- See if there's a way to check license here to avoid unnecessary work. + // We do check the license before actually _running_ setup experience items. + if enqueueSetupExperienceItems { + // Enqueue setup experience items and mark the host as being in setup experience + hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, info.Platform, r.ID, info.TeamID) + if err != nil { + return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks") } } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 9b221aa96b..ab755daa02 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -1437,7 +1437,7 @@ func TestMDMUnenrollment(t *testing.T) { } func TestMDMTokenUpdate(t *testing.T) { - ctx := context.Background() + ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium}) ds := new(mock.Store) mdmStorage := &mdmmock.MDMAppleStore{} pushFactory, _ := newMockAPNSPushProviderFactory() @@ -1600,6 +1600,164 @@ func TestMDMTokenUpdate(t *testing.T) { require.True(t, ds.SetHostMDMMigrationCompletedFuncInvoked) } +func TestMDMTokenUpdateIOS(t *testing.T) { + ctx := context.Background() + ds := new(mock.Store) + mdmStorage := &mdmmock.MDMAppleStore{} + pushFactory, _ := newMockAPNSPushProviderFactory() + pusher := nanomdm_pushsvc.New( + mdmStorage, + mdmStorage, + pushFactory, + NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)), + ) + cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher) + mdmLifecycle := mdmlifecycle.New(ds, kitlog.NewNopLogger(), newActivity) + svc := MDMAppleCheckinAndCommandService{ + ds: ds, + mdmLifecycle: mdmLifecycle, + commander: cmdr, + logger: kitlog.NewNopLogger(), + } + uuid, serial, model, wantTeamID := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1", uint(12) + + ds.GetMDMIdPAccountByHostUUIDFunc = func(ctx context.Context, hostUUID string) (*fleet.MDMIdPAccount, error) { + require.Equal(t, uuid, hostUUID) + return &fleet.MDMIdPAccount{ + UUID: "some-uuid", + Username: "some-user", + Email: "some-user@example.com", + Fullname: "Some User", + }, nil + } + + ds.NewJobFunc = func(ctx context.Context, j *fleet.Job) (*fleet.Job, error) { + return j, nil + } + + ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) { + require.Equal(t, uuid, hostUUID) + return &fleet.HostMDMCheckinInfo{ + HostID: 1337, + HardwareSerial: serial, + DisplayName: model, + InstalledFromDEP: true, + TeamID: wantTeamID, + DEPAssignedToFleet: true, + Platform: "ios", + }, nil + } + + ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) { + return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil + } + + ds.EnqueueSetupExperienceItemsFunc = func(ctx context.Context, hostPlatformLike string, hostUUID string, teamID uint) (bool, error) { + require.Equal(t, "ios", hostPlatformLike) + require.Equal(t, uuid, hostUUID) + require.Equal(t, wantTeamID, teamID) + return true, nil + } + + // DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items + err := svc.TokenUpdate( + &mdm.Request{ + Context: ctx, + EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device}, + Params: map[string]string{"enroll_reference": "abcd"}, + }, + &mdm.TokenUpdate{ + Enrollment: mdm.Enrollment{ + UDID: uuid, + }, + }, + ) + require.NoError(t, err) + require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked) + + // Non-DEP-installed, non device-type enrollment should not enqueue SetupExperience items + err = svc.TokenUpdate( + &mdm.Request{ + Context: ctx, + EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.User}, + Params: map[string]string{"enroll_reference": "abcd"}, + }, + &mdm.TokenUpdate{ + Enrollment: mdm.Enrollment{ + UDID: uuid, + }, + }, + ) + require.NoError(t, err) + require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked) + + // Non-DEP-installed without AwaitingConfiguration - should not enqueue SetupExperience items if token count is > 1 + ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) { + require.Equal(t, uuid, hostUUID) + return &fleet.HostMDMCheckinInfo{ + HostID: 1337, + HardwareSerial: serial, + DisplayName: model, + InstalledFromDEP: false, + TeamID: wantTeamID, + DEPAssignedToFleet: true, + Platform: "ios", + }, nil + } + + ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) { + return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 2}, nil + } + + err = svc.TokenUpdate( + &mdm.Request{ + Context: ctx, + EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device}, + Params: map[string]string{"enroll_reference": "abcd"}, + }, + &mdm.TokenUpdate{ + Enrollment: mdm.Enrollment{ + UDID: uuid, + }, + }, + ) + require.NoError(t, err) + require.False(t, ds.EnqueueSetupExperienceItemsFuncInvoked) + + // Non-DEP-installed without AwaitingConfiguration - should enqueue SetupExperience items if token count is 1 + ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) { + require.Equal(t, uuid, hostUUID) + return &fleet.HostMDMCheckinInfo{ + HostID: 1337, + HardwareSerial: serial, + DisplayName: model, + InstalledFromDEP: false, + TeamID: wantTeamID, + DEPAssignedToFleet: true, + Platform: "ios", + }, nil + } + + ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) { + return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil + } + + err = svc.TokenUpdate( + &mdm.Request{ + Context: ctx, + EnrollID: &mdm.EnrollID{ID: uuid, Type: mdm.Device}, + Params: map[string]string{"enroll_reference": "abcd"}, + }, + &mdm.TokenUpdate{ + Enrollment: mdm.Enrollment{ + UDID: uuid, + }, + }, + ) + require.NoError(t, err) + require.True(t, ds.EnqueueSetupExperienceItemsFuncInvoked) +} + func TestMDMCheckout(t *testing.T) { ds := new(mock.Store) mdmLifecycle := mdmlifecycle.New(ds, kitlog.NewNopLogger(), newActivity) diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 1289455d07..cd31d45c88 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -11,6 +11,7 @@ import ( "github.com/fleetdm/fleet/v4/pkg/fleetdbase" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest" @@ -114,6 +115,15 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg if _, err := a.installFleetd(ctx, args.HostUUID); err != nil { return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") } + } else { + // We shouldn't have any setup experience steps if we're not on a premium license, + // but best to check anyway plus it saves some db queries. + if license.IsPremium(ctx) { + _, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID) + if err != nil { + return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS") + } + } } return nil