Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/34042-setup-experience-on-manual-ios
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Support installation of setup-experience VPP apps on manual-enrolled iOS/iPadOS devices
34 changes: 25 additions & 9 deletions server/service/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Comment on lines 3521 to +3539
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, the logic here was "only enqueue setup experience items if the device reports the AwaitingConfiguration flag, AND it is an ios/ipad device OR it is any device that's not in the middle of a migration. After confirming that only macOS devices can be in the MigrationInProgress state, I expanded and simplified this logic to:

  1. If a device reports AwaitingConfiguration and not MigrationInProgress, enqueue setup experience items.
  2. Otherwise if it's a non-DEP-enrolled iOS/iPad device AND this is the first time it's checked in (TokenUpdateTally == 1), enqueue setup experience items

This ensures that enqueuing the items (which involves wiping out any existing items in the queue for the device) only happens once.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably also want to check the enrollment type here. I think we only want to do this if it is a Device enrollment - a User enrollment(Device)(it's a thing in nano) aka a personal or byod device both doesn't currently support VPP when enrolled in fleet and is probably not part of the spec for this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you can check the type by checking r.Type before you fetch the enrollment if that makes things cleaner. It should be set for all requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably also want to check the enrollment type here.

This is a separate check from the r.Type == mdm.Device that I'm doing? What other property should I look at?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow totally missed that. My bad - your check is totally fine


// 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")
}
}

Expand Down
160 changes: 159 additions & 1 deletion server/service/apple_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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: "[email protected]",
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)
Expand Down
10 changes: 10 additions & 0 deletions server/worker/apple_mdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading