Trigger VPP installs for iOS/iPad on enroll (#33870)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33699 Enqueues and kicks off installation process for iOS and iPadOS apps marked for installation during setup Changes file already added during earlier work ont his feature # 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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -77,7 +77,8 @@ INNER JOIN vpp_apps_teams vat
|
||||
INNER JOIN software_titles st
|
||||
ON va.title_id = st.id
|
||||
WHERE vat.install_during_setup = true
|
||||
AND vat.global_or_team_id = ?`
|
||||
AND vat.global_or_team_id = ?
|
||||
AND va.platform = ?`
|
||||
|
||||
stmtSetupScripts := `
|
||||
INSERT INTO setup_experience_status_results (
|
||||
@@ -95,6 +96,8 @@ WHERE global_or_team_id = ?`
|
||||
|
||||
var totalInsertions uint
|
||||
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
totalInsertions = 0 // reset for each attempt
|
||||
|
||||
// Clean out old statuses for the host
|
||||
if _, err := tx.ExecContext(ctx, stmtClearSetupStatus, hostUUID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "removing stale setup experience entries")
|
||||
@@ -102,23 +105,25 @@ WHERE global_or_team_id = ?`
|
||||
|
||||
// Software installers
|
||||
fleetPlatform := fleet.PlatformFromHost(hostPlatformLike)
|
||||
res, err := tx.ExecContext(ctx, stmtSoftwareInstallers, hostUUID, teamID, fleetPlatform, hostPlatformLike, hostPlatformLike)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting setup experience software installers")
|
||||
if fleetPlatform != "ios" && fleetPlatform != "ipados" {
|
||||
res, err := tx.ExecContext(ctx, stmtSoftwareInstallers, hostUUID, teamID, fleetPlatform, hostPlatformLike, hostPlatformLike)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting setup experience software installers")
|
||||
}
|
||||
inserts, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving number of inserted software installers")
|
||||
}
|
||||
totalInsertions += uint(inserts) // nolint: gosec
|
||||
}
|
||||
inserts, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving number of inserted software installers")
|
||||
}
|
||||
totalInsertions += uint(inserts) // nolint: gosec
|
||||
|
||||
// VPP apps
|
||||
if fleetPlatform == "darwin" {
|
||||
res, err = tx.ExecContext(ctx, stmtVPPApps, hostUUID, teamID)
|
||||
if fleetPlatform == "darwin" || fleetPlatform == "ios" || fleetPlatform == "ipados" {
|
||||
res, err := tx.ExecContext(ctx, stmtVPPApps, hostUUID, teamID, fleetPlatform)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting setup experience vpp apps")
|
||||
}
|
||||
inserts, err = res.RowsAffected()
|
||||
inserts, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving number of inserted vpp apps")
|
||||
}
|
||||
@@ -127,19 +132,19 @@ WHERE global_or_team_id = ?`
|
||||
|
||||
// Scripts
|
||||
if fleetPlatform == "darwin" {
|
||||
res, err = tx.ExecContext(ctx, stmtSetupScripts, hostUUID, teamID)
|
||||
res, err := tx.ExecContext(ctx, stmtSetupScripts, hostUUID, teamID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting setup experience scripts")
|
||||
}
|
||||
inserts, err = res.RowsAffected()
|
||||
inserts, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "retrieving number of inserted setup experience scripts")
|
||||
}
|
||||
totalInsertions += uint(inserts) // nolint: gosec
|
||||
}
|
||||
|
||||
// Set setup experience on darwin hosts only if they have something configured.
|
||||
if fleetPlatform == "darwin" {
|
||||
// Set setup experience on Apple hosts only if they have something configured.
|
||||
if fleetPlatform == "darwin" || fleetPlatform == "ios" || fleetPlatform == "ipados" {
|
||||
if totalInsertions > 0 {
|
||||
if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
|
||||
@@ -417,7 +422,7 @@ FROM setup_experience_status_results sesr
|
||||
LEFT JOIN setup_experience_scripts ses ON ses.id = sesr.setup_experience_script_id
|
||||
LEFT JOIN software_installers si ON si.id = sesr.software_installer_id
|
||||
LEFT JOIN vpp_apps_teams vat ON vat.id = sesr.vpp_app_team_id
|
||||
LEFT JOIN vpp_apps va ON vat.adam_id = va.adam_id
|
||||
LEFT JOIN vpp_apps va ON vat.adam_id = va.adam_id AND vat.platform = va.platform
|
||||
WHERE host_uuid = ?
|
||||
`
|
||||
var results []*fleet.SetupExperienceStatusResult
|
||||
|
||||
@@ -1102,3 +1102,11 @@ type MDMAppleEnrolledDeviceInfo struct {
|
||||
Platform string `db:"platform"`
|
||||
EnrollTeamID *uint `db:"enroll_team_id"`
|
||||
}
|
||||
|
||||
type AppleMDMVPPInstaller interface {
|
||||
// GetVPPTokenIfCanInstallVPPApps returns the host team's VPP token if the host can be a target for VPP apps
|
||||
GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *Host) (string, error)
|
||||
|
||||
// InstallVPPAppPostValidation installs a VPP app, assuming that GetVPPTokenIfCanInstallVPPApps has passed and provided a VPP token
|
||||
InstallVPPAppPostValidation(ctx context.Context, host *Host, vppApp *VPPApp, token string, opts HostSoftwareInstallOptions) (string, error)
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ func (t *HostLifecycle) turnOnApple(ctx context.Context, opts HostOptions) error
|
||||
opts.Platform,
|
||||
tmID,
|
||||
opts.EnrollReference,
|
||||
!opts.HasSetupExperienceItems,
|
||||
!opts.HasSetupExperienceItems || opts.Platform != "darwin",
|
||||
)
|
||||
return ctxerr.Wrap(ctx, err, "queue DEP post-enroll task")
|
||||
}
|
||||
|
||||
@@ -3620,7 +3620,9 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm.
|
||||
|
||||
var hasSetupExpItems bool
|
||||
if m.AwaitingConfiguration {
|
||||
if !info.MigrationInProgress {
|
||||
// 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 {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/worker"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
@@ -1869,6 +1870,524 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPCRUD() {
|
||||
checkSetupExperienceSoftware(t, "ios", team.ID, []uint{titleIDsByApp[iOSApp2]})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestSetupExperienceIOSAndIPadOS() {
|
||||
t := s.T()
|
||||
s.setSkipWorkerJobs(t)
|
||||
ctx := context.Background()
|
||||
abmOrgName := "fleet_ade_ios_ipados_team_test"
|
||||
|
||||
s.enableABM(abmOrgName)
|
||||
|
||||
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
var acResp appConfigResponse
|
||||
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
|
||||
"mdm": {
|
||||
"apple_business_manager": [{
|
||||
"organization_name": %q,
|
||||
"macos_team": %q,
|
||||
"ios_team": %q,
|
||||
"ipados_team": %q
|
||||
}]
|
||||
}
|
||||
}`, abmOrgName, team.Name, team.Name, team.Name)), http.StatusOK, &acResp)
|
||||
|
||||
orgName := "Fleet Device Management Inc."
|
||||
token := "mycooltoken"
|
||||
expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
|
||||
expDate := expTime.Format(fleet.VPPTimeFormat)
|
||||
tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
|
||||
t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
|
||||
var validToken uploadVPPTokenResponse
|
||||
s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)
|
||||
|
||||
var getVPPTokenResp getVPPTokensResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &getVPPTokenResp)
|
||||
|
||||
// Associate team to the VPP token.
|
||||
var resPatchVPP patchVPPTokensTeamsResponse
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", getVPPTokenResp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)
|
||||
|
||||
// app 1 macOS only
|
||||
macOSApp1 := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "1",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
},
|
||||
},
|
||||
Name: "App 1",
|
||||
BundleIdentifier: "a-1",
|
||||
IconURL: "https://example.com/images/1",
|
||||
LatestVersion: "1.0.0",
|
||||
}
|
||||
// App 2 supports macOS, iOS, iPadOS
|
||||
macOSApp2 := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "2",
|
||||
Platform: fleet.MacOSPlatform,
|
||||
},
|
||||
},
|
||||
Name: "App 2",
|
||||
BundleIdentifier: "b-2",
|
||||
IconURL: "https://example.com/images/2",
|
||||
LatestVersion: "2.0.0",
|
||||
}
|
||||
iOSApp2 := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "2",
|
||||
Platform: fleet.IOSPlatform,
|
||||
},
|
||||
},
|
||||
Name: "App 2",
|
||||
BundleIdentifier: "b-2",
|
||||
IconURL: "https://example.com/images/2",
|
||||
LatestVersion: "2.0.0",
|
||||
}
|
||||
iPadOSApp2 := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "2",
|
||||
Platform: fleet.IPadOSPlatform,
|
||||
},
|
||||
},
|
||||
Name: "App 2",
|
||||
BundleIdentifier: "b-2",
|
||||
IconURL: "https://example.com/images/2",
|
||||
LatestVersion: "2.0.0",
|
||||
}
|
||||
|
||||
// App 3 is iPadOS only
|
||||
iPadOSApp3 := &fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "3",
|
||||
Platform: fleet.IPadOSPlatform,
|
||||
},
|
||||
},
|
||||
Name: "App 3",
|
||||
BundleIdentifier: "c-3",
|
||||
IconURL: "https://example.com/images/3",
|
||||
LatestVersion: "3.0.0",
|
||||
}
|
||||
|
||||
expectedApps := []*fleet.VPPApp{macOSApp1, macOSApp2, iOSApp2, iPadOSApp2, iPadOSApp3}
|
||||
|
||||
var addAppResp addAppStoreAppResponse
|
||||
// Add apps
|
||||
getSoftwareTitleIDFromApp := func(app *fleet.VPPApp) uint {
|
||||
var titleID uint
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
ctx := context.Background()
|
||||
return sqlx.GetContext(ctx, q, &titleID, `SELECT title_id FROM vpp_apps WHERE adam_id = ? AND platform = ?`, app.AdamID, app.Platform)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return titleID
|
||||
}
|
||||
|
||||
titleIDsByApp := make(map[*fleet.VPPApp]uint)
|
||||
for _, app := range expectedApps {
|
||||
addAppResp = addAppStoreAppResponse{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
|
||||
&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: app.AdamID, Platform: app.Platform},
|
||||
http.StatusOK, &addAppResp)
|
||||
titleIDsByApp[app] = getSoftwareTitleIDFromApp(app)
|
||||
}
|
||||
|
||||
putSetupExperienceSoftwareForPlatform := func(t *testing.T, platform string, teamID uint, titleIDs []uint) {
|
||||
var swInstallResp putSetupExperienceSoftwareResponse
|
||||
s.DoJSON("PUT", "/api/v1/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{
|
||||
Platform: platform,
|
||||
TeamID: teamID,
|
||||
TitleIDs: titleIDs,
|
||||
}, http.StatusOK, &swInstallResp)
|
||||
}
|
||||
|
||||
// Set the 2 apps for macOS
|
||||
putSetupExperienceSoftwareForPlatform(t, "macos", team.ID, []uint{titleIDsByApp[macOSApp1], titleIDsByApp[macOSApp2]})
|
||||
|
||||
// Add an app for iOS
|
||||
putSetupExperienceSoftwareForPlatform(t, "ios", team.ID, []uint{titleIDsByApp[iOSApp2]})
|
||||
|
||||
// Add apps for iPadOS
|
||||
putSetupExperienceSoftwareForPlatform(t, "ipados", team.ID, []uint{titleIDsByApp[iPadOSApp2], titleIDsByApp[iPadOSApp3]})
|
||||
|
||||
// Add a profile
|
||||
teamProfile := mobileconfigForTest("N1", "I1")
|
||||
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{teamProfile}}, http.StatusNoContent, "team_id", fmt.Sprint(team.ID))
|
||||
|
||||
devices := []godep.Device{
|
||||
{
|
||||
Model: "iPad Pro 12.9\" (Wi-Fi Only - 3rd Gen)",
|
||||
OS: "iPadOS",
|
||||
DeviceFamily: "iPad",
|
||||
OpType: "added",
|
||||
SerialNumber: "ipad-123456",
|
||||
},
|
||||
{
|
||||
Model: "iPhone 16 Pro",
|
||||
OS: "iOS",
|
||||
DeviceFamily: "iPhone",
|
||||
OpType: "added",
|
||||
SerialNumber: "iphone-123456",
|
||||
},
|
||||
}
|
||||
|
||||
s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, devices[0].SerialNumber, devices[1].SerialNumber)
|
||||
|
||||
vppAppIDsByDeviceFamily := map[string][]*fleet.VPPApp{
|
||||
"iPhone": {iOSApp2},
|
||||
"iPad": {iPadOSApp2, iPadOSApp3},
|
||||
}
|
||||
|
||||
for _, enableReleaseManually := range []bool{true, false} {
|
||||
for _, enrollmentProfileFromDEPUsingPost := range []bool{true, false} {
|
||||
for _, device := range devices {
|
||||
t.Run(fmt.Sprintf("%sSetupExperience;enableReleaseManually=%t;EnrollmentProfileFromDEPUsingPost=%t", device.DeviceFamily, enableReleaseManually, enrollmentProfileFromDEPUsingPost), func(t *testing.T) {
|
||||
s.runDEPEnrollReleaseMobileDeviceWithVPPTest(t, device, DEPEnrollMobileTestOpts{
|
||||
ABMOrg: abmOrgName,
|
||||
EnableReleaseManually: enableReleaseManually,
|
||||
TeamID: &team.ID,
|
||||
CustomProfileIdent: "N1",
|
||||
EnrollmentProfileFromDEPUsingPost: enrollmentProfileFromDEPUsingPost,
|
||||
VppAppsToInstall: vppAppIDsByDeviceFamily[device.DeviceFamily],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DEPEnrollMobileTestOpts struct {
|
||||
ABMOrg string
|
||||
EnableReleaseManually bool
|
||||
TeamID *uint
|
||||
CustomProfileIdent string
|
||||
EnrollmentProfileFromDEPUsingPost bool
|
||||
VppAppsToInstall []*fleet.VPPApp
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) runDEPEnrollReleaseMobileDeviceWithVPPTest(t *testing.T, device godep.Device, opts DEPEnrollMobileTestOpts) {
|
||||
ctx := context.Background()
|
||||
|
||||
// set the enable release device manually option
|
||||
payload := map[string]any{
|
||||
"enable_release_device_manually": opts.EnableReleaseManually,
|
||||
"manual_agent_install": false,
|
||||
}
|
||||
if opts.TeamID != nil {
|
||||
payload["team_id"] = *opts.TeamID
|
||||
}
|
||||
|
||||
s.Do("PATCH", "/api/latest/fleet/setup_experience", json.RawMessage(jsonMustMarshal(t, payload)), http.StatusNoContent)
|
||||
t.Cleanup(func() {
|
||||
// Get back to the default state.
|
||||
payload["enable_release_device_manually"] = false
|
||||
s.Do("PATCH", "/api/latest/fleet/setup_experience", json.RawMessage(jsonMustMarshal(t, payload)), http.StatusNoContent)
|
||||
})
|
||||
|
||||
// query all hosts - none yet
|
||||
listHostsRes := listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Empty(t, listHostsRes.Hosts)
|
||||
|
||||
s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
|
||||
return map[string]*push.Response{}, nil
|
||||
}
|
||||
|
||||
s.mockDEPResponse(opts.ABMOrg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
encoder := json.NewEncoder(w)
|
||||
switch r.URL.Path {
|
||||
case "/session":
|
||||
err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
|
||||
require.NoError(t, err)
|
||||
case "/profile":
|
||||
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
case "/server/devices":
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: []godep.Device{device}})
|
||||
require.NoError(t, err)
|
||||
case "/devices/sync":
|
||||
// This endpoint is polled over time to sync devices from
|
||||
// ABM, send a repeated serial and a new one
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: []godep.Device{device}, Cursor: "foo"})
|
||||
require.NoError(t, err)
|
||||
case "/profile/devices":
|
||||
b, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var prof profileAssignmentReq
|
||||
require.NoError(t, json.Unmarshal(b, &prof))
|
||||
|
||||
var resp godep.ProfileResponse
|
||||
resp.ProfileUUID = prof.ProfileUUID
|
||||
resp.Devices = make(map[string]string, len(prof.Devices))
|
||||
for _, device := range prof.Devices {
|
||||
resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
|
||||
}
|
||||
err = encoder.Encode(resp)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}
|
||||
}))
|
||||
|
||||
// trigger a profile sync
|
||||
s.runDEPSchedule()
|
||||
|
||||
listHostsRes = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(t, listHostsRes.Hosts, 1)
|
||||
require.Equal(t, listHostsRes.Hosts[0].HardwareSerial, device.SerialNumber)
|
||||
enrolledHost := listHostsRes.Hosts[0].Host
|
||||
|
||||
t.Cleanup(func() {
|
||||
// delete the enrolled host
|
||||
err := s.ds.DeleteHost(ctx, enrolledHost.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// enroll the host
|
||||
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
||||
clientOpts := make([]mdmtest.TestMDMAppleClientOption, 0)
|
||||
if opts.EnrollmentProfileFromDEPUsingPost {
|
||||
clientOpts = append(clientOpts, mdmtest.WithEnrollmentProfileFromDEPUsingPost())
|
||||
}
|
||||
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken, clientOpts...)
|
||||
switch device.DeviceFamily {
|
||||
case "iPhone":
|
||||
mdmDevice.Model = "iPhone14,6"
|
||||
case "iPad":
|
||||
mdmDevice.Model = "iPad8,7"
|
||||
default:
|
||||
// Only expecting mobile devices for this test
|
||||
t.Fatalf("unexpected device family: %s", device.DeviceFamily)
|
||||
}
|
||||
mdmDevice.SerialNumber = device.SerialNumber
|
||||
err := mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// The host should be awaiting configuration
|
||||
awaitingConfiguration, err := s.ds.GetHostAwaitingConfiguration(ctx, mdmDevice.UUID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, awaitingConfiguration)
|
||||
|
||||
// run the worker to process the DEP enroll request
|
||||
s.runWorker()
|
||||
|
||||
// run the cron to assign configuration profiles
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
var cmds []*micromdm.CommandPayload
|
||||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
|
||||
// For reporting back via InstalledApplicationList
|
||||
installedVPPApps := make([]fleet.Software, 0, len(opts.VppAppsToInstall))
|
||||
// For verifying number of installs
|
||||
installedApps := make(map[string]int, len(opts.VppAppsToInstall))
|
||||
|
||||
var installProfileCount, installAppCount, refetchVerifyCount, otherCount int
|
||||
var profileCustomSeen, profileFleetCASeen, unexpectedProfileSeen bool
|
||||
|
||||
// Can be useful for debugging
|
||||
logCommands := false
|
||||
for cmd != nil {
|
||||
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
|
||||
if strings.HasPrefix(cmd.CommandUUID, fleet.RefetchAppsCommandUUID()) {
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch cmd.Command.RequestType {
|
||||
case "InstallProfile":
|
||||
if logCommands {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
|
||||
}
|
||||
installProfileCount++
|
||||
if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), //nolint:gocritic // ignore ifElseChain
|
||||
fmt.Sprintf("<string>%s</string>", opts.CustomProfileIdent)) {
|
||||
profileCustomSeen = true
|
||||
} else if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)) {
|
||||
unexpectedProfileSeen = true
|
||||
} else if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)) {
|
||||
profileFleetCASeen = true
|
||||
} else if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
|
||||
strings.Contains(string(fullCmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant") {
|
||||
unexpectedProfileSeen = true
|
||||
}
|
||||
case "InstallApplication":
|
||||
if logCommands {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, fmt.Sprint(*fullCmd.Command.InstallApplication.ITunesStoreID))
|
||||
}
|
||||
for _, app := range opts.VppAppsToInstall {
|
||||
if app.AdamID == fmt.Sprint(*fullCmd.Command.InstallApplication.ITunesStoreID) {
|
||||
installedVPPApps = append(installedVPPApps, fleet.Software{BundleIdentifier: app.BundleIdentifier, Name: app.Name, Version: app.LatestVersion, Installed: true})
|
||||
installedApps[app.AdamID]++
|
||||
}
|
||||
}
|
||||
installAppCount++
|
||||
|
||||
case "InstallEnterpriseApplication":
|
||||
if logCommands {
|
||||
if fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL)
|
||||
} else {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
|
||||
}
|
||||
}
|
||||
case "InstalledApplicationList":
|
||||
if logCommands {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
|
||||
}
|
||||
// If we are polling to verify the install, we should get an
|
||||
// InstalledApplicationList command instead of an InstallApplication command.
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(
|
||||
mdmDevice.UUID,
|
||||
cmd.CommandUUID,
|
||||
installedVPPApps,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
// TODO: We don't actually normally get a command back from the acknowledgement of the InstalledAppList
|
||||
// but we'll get additional install commands if we follow it up with an idle. Is this a bug? I think it
|
||||
// may be because of how we handle activating the next upcoming activity?
|
||||
if cmd == nil {
|
||||
cmd, err = mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
continue
|
||||
default:
|
||||
if logCommands {
|
||||
fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType)
|
||||
}
|
||||
otherCount++
|
||||
}
|
||||
|
||||
cmds = append(cmds, &fullCmd)
|
||||
|
||||
if cmd.Command.RequestType == "InstallApplication" {
|
||||
pending, err := s.ds.GetQueuedJobs(ctx, 5, time.Now().UTC().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
for _, job := range pending {
|
||||
if job.Name == "apple_software" {
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before = ? WHERE id = ?`, time.Now().Add(-1*time.Minute).UTC(), job.ID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
// Run the worker to process the VPP verification job before acking so that the Verify command is waiting for us
|
||||
s.runWorker()
|
||||
}
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// expected commands: install CA, install profile (only the custom one),
|
||||
// not expected: account configuration, since enrollment_reference not set
|
||||
require.Len(t, cmds, 2+len(opts.VppAppsToInstall))
|
||||
|
||||
require.Equal(t, 2, installProfileCount)
|
||||
require.True(t, profileCustomSeen)
|
||||
require.True(t, profileFleetCASeen)
|
||||
require.Equal(t, false, unexpectedProfileSeen)
|
||||
|
||||
require.Equal(t, len(opts.VppAppsToInstall), installAppCount)
|
||||
require.Equal(t, len(opts.VppAppsToInstall), len(installedApps))
|
||||
|
||||
// Each expected app should be installed exactly once
|
||||
for _, app := range opts.VppAppsToInstall {
|
||||
require.Equal(t, 1, installedApps[app.AdamID])
|
||||
}
|
||||
|
||||
require.Equal(t, 0, otherCount)
|
||||
|
||||
pendingReleaseJobs := []*fleet.Job{}
|
||||
if opts.EnableReleaseManually {
|
||||
// get the worker's pending job from the future, there should not be any
|
||||
// because it needs to be released manually
|
||||
pending, err := s.ds.GetQueuedJobs(ctx, 5, time.Now().UTC().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
for _, job := range pending {
|
||||
if job.Name == "apple_mdm" && strings.Contains(string(*job.Args), string(worker.AppleMDMPostDEPReleaseDeviceTask)) {
|
||||
pendingReleaseJobs = append(pendingReleaseJobs, job)
|
||||
} else if job.Name == "apple_software" {
|
||||
// Just delete the job for now to keep things clean
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `DELETE FROM jobs WHERE id = ?`, job.ID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
require.Len(t, pendingReleaseJobs, 0)
|
||||
} else {
|
||||
// otherwise the device release job should be enqueued
|
||||
pending, err := s.ds.GetQueuedJobs(ctx, 5, time.Now().UTC().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
for _, job := range pending {
|
||||
if job.Name == "apple_mdm" && strings.Contains(string(*job.Args), string(worker.AppleMDMPostDEPReleaseDeviceTask)) {
|
||||
pendingReleaseJobs = append(pendingReleaseJobs, job)
|
||||
} else if job.Name == "apple_software" {
|
||||
// Just delete the job for now to keep things clean
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `DELETE FROM jobs WHERE id = ?`, job.ID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
require.Len(t, pendingReleaseJobs, 1)
|
||||
require.Equal(t, "apple_mdm", pendingReleaseJobs[0].Name)
|
||||
require.Contains(t, string(*pendingReleaseJobs[0].Args), worker.AppleMDMPostDEPReleaseDeviceTask)
|
||||
|
||||
// make the pending job ready to run immediately and run the job
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before = ? WHERE id = ?`, time.Now().Add(-1*time.Minute).UTC(), pendingReleaseJobs[0].ID)
|
||||
return err
|
||||
})
|
||||
|
||||
s.runWorker()
|
||||
|
||||
// make the device process the commands, it should receive the
|
||||
// DeviceConfigured one.
|
||||
cmds = cmds[:0]
|
||||
cmd, err = mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
var fullCmd micromdm.CommandPayload
|
||||
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
||||
cmds = append(cmds, &fullCmd)
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.Len(t, cmds, 1)
|
||||
var deviceConfiguredCount int
|
||||
for _, cmd := range cmds {
|
||||
if strings.HasPrefix(cmd.CommandUUID, fleet.RefetchAppsCommandUUIDPrefix) || strings.HasPrefix(cmd.CommandUUID, fleet.VerifySoftwareInstallVPPPrefix) {
|
||||
refetchVerifyCount++
|
||||
continue
|
||||
}
|
||||
switch cmd.Command.RequestType {
|
||||
case "DeviceConfigured":
|
||||
deviceConfiguredCount++
|
||||
default:
|
||||
otherCount++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, deviceConfiguredCount)
|
||||
require.Equal(t, 0, otherCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetupExperienceEndpointsPlatformIsolation tests that setting the setup experience software items
|
||||
// for one platform doesn't remove the items for another platform on the same team.
|
||||
func (s *integrationMDMTestSuite) TestSetupExperienceEndpointsPlatformIsolation() {
|
||||
|
||||
@@ -365,7 +365,14 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
||||
{Name: fleet.MDMAssetSCEPChallenge, Value: []byte(s.scepChallenge)},
|
||||
}, nil)
|
||||
require.NoError(s.T(), err)
|
||||
users, server := RunServerForTestsWithDS(s.T(), s.ds, &serverConfig)
|
||||
|
||||
svc, ctx := NewTestService(s.T(), s.ds, fleetCfg, &serverConfig)
|
||||
// This is a bit of a code smell but I don't see a better way to initialize this for the tests. The
|
||||
// initialization pattern works fine in our normal fleet server setup
|
||||
appleMDMJob.VPPInstaller = svc
|
||||
|
||||
users, server := RunServerForTestsWithServiceWithDS(s.T(), ctx, s.ds, svc, &serverConfig)
|
||||
|
||||
s.server = server
|
||||
s.users = users
|
||||
s.token = s.getTestAdminToken()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
@@ -43,6 +44,7 @@ type AppleMDM struct {
|
||||
Log kitlog.Logger
|
||||
Commander *apple_mdm.MDMAppleCommander
|
||||
BootstrapPackageStore fleet.MDMBootstrapPackageStore
|
||||
VPPInstaller fleet.AppleMDMVPPInstaller
|
||||
}
|
||||
|
||||
// Name returns the name of the job.
|
||||
@@ -154,6 +156,13 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
|
||||
if bootstrapCmdUUID != "" {
|
||||
awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
|
||||
}
|
||||
} else {
|
||||
// TODO: We likely want to wait for the actual installs to complete not just the commands to be ack'd
|
||||
commandUUIDs, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
|
||||
}
|
||||
awaitCmdUUIDs = append(awaitCmdUUIDs, commandUUIDs...)
|
||||
}
|
||||
|
||||
if ref := args.EnrollReference; ref != "" {
|
||||
@@ -438,6 +447,102 @@ func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) (string,
|
||||
return cmdUUID, nil
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context, hostUUID string) ([]string, error) {
|
||||
statuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
|
||||
}
|
||||
|
||||
var appsPending []*fleet.SetupExperienceStatusResult
|
||||
commandUUIDs := []string{}
|
||||
for _, status := range statuses {
|
||||
if err := status.IsValid(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "invalid row")
|
||||
}
|
||||
|
||||
switch {
|
||||
case status.VPPAppTeamID != nil:
|
||||
if status.Status == fleet.SetupExperienceStatusPending {
|
||||
appsPending = append(appsPending, status)
|
||||
}
|
||||
case status.SetupExperienceScriptID != nil, status.SoftwareInstallerID != nil:
|
||||
status.Status = fleet.SetupExperienceStatusFailure
|
||||
err = a.Datastore.UpdateSetupExperienceStatusResult(ctx, status)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating setup experience status result to failure")
|
||||
}
|
||||
// If we enqueued a non-VPP item for an iOS/iPadOS device, it likely a code bug
|
||||
level.Error(a.Log).Log("msg", "unexpected setup experience item for iOS/iPadOS device, only VPP apps are supported", "host_uuid", hostUUID, "status_id", status.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(appsPending) > 0 {
|
||||
// enqueue vpp apps
|
||||
// TODO Is there a better way to get a host by UUID? This is a somewhat "wide" search which feels unnecessary
|
||||
host, err := a.Datastore.HostByIdentifier(ctx, hostUUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "retrieving host by UUID")
|
||||
}
|
||||
for _, app := range appsPending {
|
||||
vppAppID, err := app.VPPAppID()
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "constructing vpp app details for installation")
|
||||
}
|
||||
|
||||
if app.SoftwareTitleID == nil {
|
||||
return nil, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", app.ID)
|
||||
}
|
||||
|
||||
vppApp := &fleet.VPPApp{
|
||||
TitleID: *app.SoftwareTitleID,
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: *vppAppID,
|
||||
},
|
||||
}
|
||||
|
||||
opts := fleet.HostSoftwareInstallOptions{
|
||||
SelfService: false,
|
||||
ForSetupExperience: true,
|
||||
}
|
||||
|
||||
cmdUUID, err := a.installSoftwareFromVPP(ctx, host, vppApp, true, opts)
|
||||
|
||||
app.NanoCommandUUID = &cmdUUID
|
||||
app.Status = fleet.SetupExperienceStatusRunning
|
||||
|
||||
if err != nil {
|
||||
// if we get an error (e.g. no available licenses) while attempting to enqueue the
|
||||
// install, then we should immediately go to an error state so setup experience
|
||||
// isn't blocked.
|
||||
level.Error(a.Log).Log("msg", "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", app.VPPAppAdamID)
|
||||
app.Status = fleet.SetupExperienceStatusFailure
|
||||
app.Error = ptr.String(err.Error())
|
||||
} else {
|
||||
commandUUIDs = append(commandUUIDs, cmdUUID)
|
||||
}
|
||||
if err := a.Datastore.UpdateSetupExperienceStatusResult(ctx, app); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commandUUIDs, nil
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, opts fleet.HostSoftwareInstallOptions) (string, error) {
|
||||
// Should not happen in the normal course of events but can happen in tests
|
||||
// and likely indicates things weren't initialized properly.
|
||||
if a.VPPInstaller == nil {
|
||||
return "", errors.New("VPP installer not configured")
|
||||
}
|
||||
token, err := a.VPPInstaller.GetVPPTokenIfCanInstallVPPApps(ctx, appleDevice, host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return a.VPPInstaller.InstallVPPAppPostValidation(ctx, host, vppApp, token, opts)
|
||||
}
|
||||
|
||||
func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) (string, error) {
|
||||
// GetMDMAppleBootstrapPackageMeta expects team id 0 for no team
|
||||
var tmID uint
|
||||
|
||||
+262
-16
@@ -17,6 +17,7 @@ import (
|
||||
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
mock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -39,6 +40,34 @@ func (m mockPusher) Push(context.Context, []string) (map[string]*nanomdm_push.Re
|
||||
return res, m.err
|
||||
}
|
||||
|
||||
type installAppResponse struct {
|
||||
CommandUUID string
|
||||
Error error
|
||||
}
|
||||
|
||||
type mockVPPInstaller struct {
|
||||
t *testing.T
|
||||
installedApps []*fleet.VPPApp
|
||||
appInstallResponses map[string]installAppResponse
|
||||
getTokenErr error
|
||||
}
|
||||
|
||||
func (m *mockVPPInstaller) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) {
|
||||
require.True(m.t, appleDevice)
|
||||
if m.getTokenErr != nil {
|
||||
return "", m.getTokenErr
|
||||
}
|
||||
return "valid-token", nil
|
||||
}
|
||||
|
||||
func (m *mockVPPInstaller) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, opts fleet.HostSoftwareInstallOptions) (string, error) {
|
||||
require.True(m.t, opts.ForSetupExperience)
|
||||
resp, ok := m.appInstallResponses[vppApp.AdamID]
|
||||
require.True(m.t, ok)
|
||||
m.installedApps = append(m.installedApps, vppApp)
|
||||
return resp.CommandUUID, resp.Error
|
||||
}
|
||||
|
||||
func TestAppleMDM(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -58,14 +87,14 @@ func TestAppleMDM(t *testing.T) {
|
||||
|
||||
testOrgName := "fleet-test"
|
||||
|
||||
createEnrolledHost := func(t *testing.T, i int, teamID *uint, depAssignedToFleet bool) *fleet.Host {
|
||||
createEnrolledHost := func(t *testing.T, i int, teamID *uint, depAssignedToFleet bool, platform string) *fleet.Host {
|
||||
// create the host
|
||||
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: fmt.Sprintf("test-host%d-name", i),
|
||||
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||
UUID: uuid.New().String(),
|
||||
Platform: "darwin",
|
||||
Platform: platform,
|
||||
HardwareSerial: fmt.Sprintf("serial-%d", i),
|
||||
TeamID: teamID,
|
||||
})
|
||||
@@ -149,7 +178,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
w.Register(mdmWorker)
|
||||
|
||||
// create a host and enqueue the job
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -179,7 +208,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
w.Register(mdmWorker)
|
||||
|
||||
// create a host and enqueue the job
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -202,7 +231,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -237,7 +266,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
t.Cleanup(func() { mysql.TruncateTables(t, ds) })
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
enableManualRelease(t, nil)
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
@@ -272,7 +301,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
||||
Name: "custom-bootstrap",
|
||||
TeamID: 0, // no-team
|
||||
@@ -321,7 +350,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true)
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true, "darwin")
|
||||
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
||||
Name: "custom-team-bootstrap",
|
||||
TeamID: tm.ID,
|
||||
@@ -371,7 +400,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
enableManualRelease(t, &tm.ID)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true)
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true, "darwin")
|
||||
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
||||
Name: "custom-team-bootstrap",
|
||||
TeamID: tm.ID,
|
||||
@@ -417,7 +446,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -460,7 +489,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -513,7 +542,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
_, err = ds.SaveTeam(ctx, tm)
|
||||
require.NoError(t, err)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true)
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -547,7 +576,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -578,7 +607,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -614,7 +643,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -709,7 +738,7 @@ func TestAppleMDM(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
h := createEnrolledHost(t, 1, nil, true)
|
||||
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
Datastore: ds,
|
||||
@@ -769,6 +798,223 @@ func TestAppleMDM(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 0)
|
||||
})
|
||||
|
||||
t.Run("installs enqueued VPP apps", func(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true, "ios")
|
||||
|
||||
expectedAppInstalls := []*fleet.VPPApp{}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
idx := fmt.Sprint(i)
|
||||
vppApp := &fleet.VPPApp{
|
||||
Name: "vpp_worker-" + idx, LatestVersion: "1.0.0", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "depworker-" + idx, Platform: fleet.IOSPlatform}},
|
||||
BundleIdentifier: "b" + idx,
|
||||
}
|
||||
vppAppWithTeam, err := ds.InsertVPPAppWithTeam(ctx, vppApp, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
expectedAppInstalls = append(expectedAppInstalls, vppAppWithTeam)
|
||||
}
|
||||
|
||||
appInstallResponses := make(map[string]installAppResponse, len(expectedAppInstalls))
|
||||
|
||||
for _, appWithTeam := range expectedAppInstalls {
|
||||
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `
|
||||
INSERT INTO setup_experience_status_results (
|
||||
host_uuid,
|
||||
name,
|
||||
status,
|
||||
vpp_app_team_id
|
||||
) VALUES (?, ?, ?, ?)
|
||||
`
|
||||
_, err = q.ExecContext(ctx, stmt, h.UUID, appWithTeam.Name, fleet.SetupExperienceStatusPending, appWithTeam.VPPAppTeam.AppTeamID)
|
||||
return err
|
||||
})
|
||||
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: uuid.NewString(), Error: nil}
|
||||
}
|
||||
|
||||
vppInstaller := &mockVPPInstaller{t: t, appInstallResponses: appInstallResponses}
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
VPPInstaller: vppInstaller,
|
||||
Datastore: ds,
|
||||
Log: nopLog,
|
||||
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
||||
}
|
||||
w := NewWorker(ds, nopLog)
|
||||
w.Register(mdmWorker)
|
||||
|
||||
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, h.Platform, nil, "", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// run the worker, should succeed
|
||||
err = w.ProcessJobs(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the job's not_before allows it to be returned if it were to run
|
||||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, jobs)
|
||||
var releaseJob *fleet.Job
|
||||
for _, job := range jobs {
|
||||
if job.Name == appleMDMJobName {
|
||||
// THere should only be one release job
|
||||
require.Nil(t, releaseJob)
|
||||
releaseJob = job
|
||||
}
|
||||
}
|
||||
// We should have found a release job
|
||||
require.NotNil(t, releaseJob)
|
||||
// It should be the release task
|
||||
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
// And it should contain the command IDs for the installs
|
||||
expectedAdamIDs := make([]string, 0, len(expectedAppInstalls))
|
||||
installedAdamIDs := make([]string, 0, len(vppInstaller.installedApps))
|
||||
for _, app := range expectedAppInstalls {
|
||||
require.Contains(t, string(*releaseJob.Args), appInstallResponses[app.AdamID].CommandUUID)
|
||||
expectedAdamIDs = append(expectedAdamIDs, app.AdamID)
|
||||
}
|
||||
for _, installed := range vppInstaller.installedApps {
|
||||
installedAdamIDs = append(installedAdamIDs, installed.AdamID)
|
||||
}
|
||||
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
|
||||
|
||||
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(expectedAppInstalls))
|
||||
for _, result := range results {
|
||||
require.Equal(t, fleet.SetupExperienceStatusRunning, result.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("marks failed VPP installs as failed, runs all others", func(t *testing.T) {
|
||||
mysql.SetTestABMAssets(t, ds, testOrgName)
|
||||
test.CreateInsertGlobalVPPToken(t, ds)
|
||||
defer mysql.TruncateTables(t, ds)
|
||||
badCommandUUID := "bad-command-uuid"
|
||||
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
h := createEnrolledHost(t, 1, &tm.ID, true, "ios")
|
||||
|
||||
expectedAppInstalls := []*fleet.VPPApp{}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
idx := fmt.Sprint(i)
|
||||
vppApp := &fleet.VPPApp{
|
||||
Name: "vpp_worker-" + idx, LatestVersion: "1.0.0", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "depworker-" + idx, Platform: fleet.IOSPlatform}},
|
||||
BundleIdentifier: "b" + idx,
|
||||
}
|
||||
vppAppWithTeam, err := ds.InsertVPPAppWithTeam(ctx, vppApp, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
expectedAppInstalls = append(expectedAppInstalls, vppAppWithTeam)
|
||||
}
|
||||
|
||||
appInstallResponses := make(map[string]installAppResponse, len(expectedAppInstalls))
|
||||
|
||||
for _, appWithTeam := range expectedAppInstalls {
|
||||
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `
|
||||
INSERT INTO setup_experience_status_results (
|
||||
host_uuid,
|
||||
name,
|
||||
status,
|
||||
vpp_app_team_id
|
||||
) VALUES (?, ?, ?, ?)
|
||||
`
|
||||
_, err = q.ExecContext(ctx, stmt, h.UUID, appWithTeam.Name, fleet.SetupExperienceStatusPending, appWithTeam.VPPAppTeam.AppTeamID)
|
||||
return err
|
||||
})
|
||||
if len(appInstallResponses) == 0 {
|
||||
// first one, simulate a failure. It shouldn't actually
|
||||
// return a command UUID here but even if it does we
|
||||
// should not wait on it
|
||||
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: badCommandUUID, Error: errors.New("test error")}
|
||||
continue
|
||||
}
|
||||
// rest succeed
|
||||
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: uuid.NewString(), Error: nil}
|
||||
}
|
||||
|
||||
vppInstaller := &mockVPPInstaller{t: t, appInstallResponses: appInstallResponses}
|
||||
|
||||
mdmWorker := &AppleMDM{
|
||||
VPPInstaller: vppInstaller,
|
||||
Datastore: ds,
|
||||
Log: nopLog,
|
||||
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
||||
}
|
||||
w := NewWorker(ds, nopLog)
|
||||
w.Register(mdmWorker)
|
||||
|
||||
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, h.Platform, nil, "", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// run the worker, should succeed
|
||||
err = w.ProcessJobs(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the job's not_before allows it to be returned if it were to run
|
||||
// again
|
||||
time.Sleep(time.Second)
|
||||
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, jobs)
|
||||
var releaseJob *fleet.Job
|
||||
for _, job := range jobs {
|
||||
if job.Name == appleMDMJobName {
|
||||
// THere should only be one release job
|
||||
require.Nil(t, releaseJob)
|
||||
releaseJob = job
|
||||
}
|
||||
}
|
||||
// We should have found a release job
|
||||
require.NotNil(t, releaseJob)
|
||||
// It should be the release task
|
||||
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
||||
|
||||
// And it should contain the command IDs for the installs that didn't error
|
||||
expectedAdamIDs := make([]string, 0, len(expectedAppInstalls))
|
||||
installedAdamIDs := make([]string, 0, len(vppInstaller.installedApps))
|
||||
for _, app := range expectedAppInstalls {
|
||||
expectedAdamIDs = append(expectedAdamIDs, app.AdamID)
|
||||
if appInstallResponses[app.AdamID].Error != nil {
|
||||
// this one failed, so it should not be in the release command
|
||||
continue
|
||||
}
|
||||
require.Contains(t, string(*releaseJob.Args), appInstallResponses[app.AdamID].CommandUUID)
|
||||
}
|
||||
require.NotContains(t, string(*releaseJob.Args), badCommandUUID)
|
||||
for _, installed := range vppInstaller.installedApps {
|
||||
installedAdamIDs = append(installedAdamIDs, installed.AdamID)
|
||||
}
|
||||
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
|
||||
|
||||
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(expectedAppInstalls))
|
||||
for _, result := range results {
|
||||
require.NotNil(t, result.VPPAppAdamID)
|
||||
if *result.VPPAppAdamID == expectedAppInstalls[0].AdamID {
|
||||
// this is the one we simulated a failure for
|
||||
require.Equal(t, fleet.SetupExperienceStatusFailure, result.Status)
|
||||
continue
|
||||
}
|
||||
require.Equal(t, fleet.SetupExperienceStatusRunning, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSignedURL(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user