Add integration tests for the setup experience flow with automatic and forced release (#22909)

This commit is contained in:
Martin Angers
2024-10-14 16:41:06 -04:00
committed by GitHub
parent 5228a5fc64
commit b42f5ffbd0
4 changed files with 529 additions and 91 deletions
File diff suppressed because one or more lines are too long
+29 -88
View File
@@ -12,7 +12,6 @@ import (
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
@@ -8596,14 +8595,14 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
SelfService: false,
TeamID: &team1.ID,
}
s.uploadSoftwareInstaller(payloadRubyTm1, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadRubyTm1, http.StatusOK, "")
payloadEmacs := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "emacs.deb",
SelfService: true,
}
s.uploadSoftwareInstaller(payloadEmacs, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadEmacs, http.StatusOK, "")
payloadVim := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
@@ -8611,7 +8610,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
SelfService: true,
TeamID: ptr.Uint(0),
}
s.uploadSoftwareInstaller(payloadVim, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadVim, http.StatusOK, "")
resp = listSoftwareTitlesResponse{}
s.DoJSON(
@@ -8631,7 +8630,7 @@ func (s *integrationEnterpriseTestSuite) TestAllSoftwareTitles() {
Filename: "ruby_arm64.deb",
TeamID: &team2.ID,
}
s.uploadSoftwareInstaller(payloadRubyTm2, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadRubyTm2, http.StatusOK, "")
// We should only see the one we uploaded to team 1
resp = listSoftwareTitlesResponse{}
@@ -10056,7 +10055,7 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
Filename: "ruby.deb",
Version: "1:2.5.1",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, "ruby", "deb_packages")
// update it to be self-service
@@ -10198,7 +10197,7 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
Filename: "dummy_installer.pkg",
Version: "0.0.2", // The version can be anything -- we match on title
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// Get software available for install
getHostSw = getHostSoftwareResponse{}
@@ -10320,7 +10319,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
Platform: "linux",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null, "self_service": false}`, 0)
@@ -10329,7 +10328,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
_, titleID := checkSoftwareInstaller(t, payload)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
// orbit-downloading fails with invalid orbit node key
s.Do("POST", "/api/fleet/orbit/software_install/package?alt=media", orbitDownloadSoftwareInstallerRequest{
@@ -10368,7 +10367,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
Platform: "linux",
SelfService: true,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// check the software installer
installerID, titleID := checkSoftwareInstaller(t, payload)
@@ -10377,7 +10376,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", *payload.TeamID))
@@ -10483,7 +10482,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
Platform: "linux",
SelfService: true,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// check the software installer
installerID, titleID := checkSoftwareInstaller(t, payload)
@@ -10492,7 +10491,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": 0, "self_service": true}`), 0)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", 0))
@@ -10578,7 +10577,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
Platform: "linux",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
logger := kitlog.NewLogfmtLogger(os.Stderr)
@@ -11574,7 +11573,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
Title: "ruby",
TeamID: teamID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
// Get title with software installer
@@ -11591,7 +11590,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
Title: "DummyApp.app",
TeamID: teamID,
}
s.uploadSoftwareInstaller(payloadDummy, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadDummy, http.StatusOK, "")
pkgTitleID := getSoftwareTitleID(t, s.ds, payloadDummy.Title, "apps")
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", pkgTitleID), nil, http.StatusOK, &respTitle, "team_id",
fmt.Sprintf("%d", *teamID))
@@ -11941,7 +11940,7 @@ func (s *integrationEnterpriseTestSuite) TestSelfServiceSoftwareInstall() {
Title: "ruby",
SelfService: false,
}
s.uploadSoftwareInstaller(payloadNoSS, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadNoSS, http.StatusOK, "")
titleIDNoSS := getSoftwareTitleID(t, s.ds, payloadNoSS.Title, "deb_packages")
payloadSS := &fleet.UploadSoftwareInstallerPayload{
@@ -11952,7 +11951,7 @@ func (s *integrationEnterpriseTestSuite) TestSelfServiceSoftwareInstall() {
Title: "emacs",
SelfService: true,
}
s.uploadSoftwareInstaller(payloadSS, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payloadSS, http.StatusOK, "")
titleIDSS := getSoftwareTitleID(t, s.ds, payloadSS.Title, "deb_packages")
// cannot self-install if software installer does not allow it
@@ -12022,7 +12021,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
Filename: "ruby.deb",
Title: "ruby",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
payload2 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install script 2",
@@ -12031,7 +12030,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
Filename: "vim.deb",
Title: "vim",
}
s.uploadSoftwareInstaller(payload2, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload2, http.StatusOK, "")
titleID2 := getSoftwareTitleID(t, s.ds, payload2.Title, "deb_packages")
payload3 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install script 3",
@@ -12040,7 +12039,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
Filename: "emacs.deb",
Title: "emacs",
}
s.uploadSoftwareInstaller(payload3, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload3, http.StatusOK, "")
titleID3 := getSoftwareTitleID(t, s.ds, payload3.Title, "deb_packages")
latestInstallUUID := func() string {
@@ -12276,64 +12275,6 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
require.EqualValues(t, 0, *scriptRes.ExitCode)
}
func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(
payload *fleet.UploadSoftwareInstallerPayload,
expectedStatus int,
expectedError string,
) {
t := s.T()
t.Helper()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
f := openFile(payload.Filename)
defer f.Close()
payload.InstallerFile = f
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the software field
fw, err := w.CreateFormFile("software", payload.Filename)
require.NoError(t, err)
n, err := io.Copy(fw, payload.InstallerFile)
require.NoError(t, err)
require.NotZero(t, n)
// add the team_id field
if payload.TeamID != nil {
require.NoError(t, w.WriteField("team_id", fmt.Sprintf("%d", *payload.TeamID)))
}
// add the remaining fields
require.NoError(t, w.WriteField("install_script", payload.InstallScript))
require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery))
require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript))
require.NoError(t, w.WriteField("uninstall_script", payload.UninstallScript))
if payload.SelfService {
require.NoError(t, w.WriteField("self_service", "true"))
}
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
"Accept": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", s.token),
}
r := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/package", b.Bytes(), expectedStatus, headers)
defer r.Body.Close()
if expectedError != "" {
errMsg := extractServerErrorText(r.Body)
require.Contains(t, errMsg, expectedError)
}
}
func getSoftwareTitleID(t *testing.T, ds *mysql.Datastore, title, source string) uint {
var id uint
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@@ -12559,7 +12500,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
Filename: "dummy_installer.pkg",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
resp := listSoftwareTitlesResponse{}
s.DoJSON(
@@ -12663,7 +12604,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGNoVersion() {
Filename: "no_version.pkg",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(payload, http.StatusBadRequest, "Couldn't add. Fleet couldn't read the version from no_version.pkg.")
s.uploadSoftwareInstaller(t, payload, http.StatusBadRequest, "Couldn't add. Fleet couldn't read the version from no_version.pkg.")
}
// 1. host reports software
@@ -12731,7 +12672,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareAlreadyReported() {
Filename: "dummy_installer.pkg",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
resp = listSoftwareTitlesResponse{}
s.DoJSON(
@@ -12795,7 +12736,7 @@ func (s *integrationEnterpriseTestSuite) TestPKGSoftwareReconciliation() {
Filename: "dummy_installer.pkg",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
resp := listSoftwareTitlesResponse{}
s.DoJSON(
@@ -13704,7 +13645,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
Filename: "dummy_installer.pkg",
TeamID: &team1.ID,
}
s.uploadSoftwareInstaller(pkgPayload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, pkgPayload, http.StatusOK, "")
// Get software title ID of the uploaded installer.
resp := listSoftwareTitlesResponse{}
s.DoJSON(
@@ -13767,7 +13708,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
s.token = adminToken
})
s.token = adminTeam1Session.Key
s.uploadSoftwareInstaller(rubyPayload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, rubyPayload, http.StatusOK, "")
s.token = adminToken
err = s.ds.DeleteUser(ctx, adminTeam1.ID)
require.NoError(t, err)
@@ -13812,7 +13753,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
// author (the admin that uploaded the installer).
SelfService: true,
}
s.uploadSoftwareInstaller(fleetOsqueryPayload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, fleetOsqueryPayload, http.StatusOK, "")
// Get software title ID of the uploaded installer.
resp = listSoftwareTitlesResponse{}
s.DoJSON(
@@ -14829,7 +14770,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIden
Filename: "dummy_installer.pkg",
Version: "0.0.2",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
}
func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() {
@@ -14847,7 +14788,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() {
Filename: "ruby.rpm",
Title: "ruby",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "rpm_packages")
latestInstallUUID := func() string {
+437
View File
@@ -1301,3 +1301,440 @@ func (s *integrationMDMTestSuite) TestSetupExperienceScript() {
// try deleting the team script again
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/setup_experience/script?team_id=%d", tm.ID), nil, http.StatusOK) // TODO: confirm if we want to return not found
}
func (s *integrationMDMTestSuite) createTeamDeviceForSetupExperienceWithProfileSoftwareAndScript() (device godep.Device, host *fleet.Host, tm *fleet.Team) {
t := s.T()
ctx := context.Background()
// enroll a device in a team with software to install and a script to execute
s.enableABM("fleet-setup-experience")
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
teamDevice := godep.Device{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"}
// add a team 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(tm.ID))
// add a macOS software to install
payloadDummy := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "dummy_installer.pkg",
Title: "DummyApp.app",
TeamID: &tm.ID,
}
s.uploadSoftwareInstaller(t, payloadDummy, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payloadDummy.Title, "apps")
var swInstallResp putSetupExperienceSoftwareResponse
s.DoJSON("PUT", "/api/v1/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{TeamID: tm.ID, TitleIDs: []uint{titleID}}, http.StatusOK, &swInstallResp)
// add a script to execute
body, headers := generateNewScriptMultipartRequest(t,
"script.sh", []byte(`echo "hello"`), s.token, map[string][]string{"team_id": {fmt.Sprintf("%d", tm.ID)}})
s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers)
// no bootstrap package, no custom setup assistant (those are already tested
// in the DEPEnrollReleaseDevice tests).
s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
return map[string]*push.Response{}, nil
}
s.mockDEPResponse("fleet-setup-experience", 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{teamDevice}})
require.NoError(t, err)
case "/devices/sync":
// This endpoint is polled over time to sync devices from
// ABM, send a repeated serial
err := encoder.Encode(godep.DeviceResponse{Devices: []godep.Device{teamDevice}, 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()
// the (ghost) host now exists
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, teamDevice.SerialNumber)
enrolledHost := listHostsRes.Hosts[0].Host
// transfer it to the team
s.Do("POST", "/api/v1/fleet/hosts/transfer",
addHostsToTeamRequest{TeamID: &tm.ID, HostIDs: []uint{enrolledHost.ID}}, http.StatusOK)
return teamDevice, enrolledHost, tm
}
func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAutoRelease() {
t := s.T()
ctx := context.Background()
teamDevice, enrolledHost, _ := s.createTeamDeviceForSetupExperienceWithProfileSoftwareAndScript()
// enroll the host
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
mdmDevice.SerialNumber = teamDevice.SerialNumber
err := mdmDevice.Enroll()
require.NoError(t, err)
// run the worker to process the DEP enroll request
s.runWorker()
// run the worker to assign configuration profiles
s.awaitTriggerProfileSchedule(t)
var cmds []*micromdm.CommandPayload
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
var fullCmd micromdm.CommandPayload
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
// Can be useful for debugging
//switch cmd.Command.RequestType {
//case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
//case "InstallEnterpriseApplication":
// 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)
// }
//default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
//}
cmds = append(cmds, &fullCmd)
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
// expected commands: install fleetd (install enterprise), install profiles
// (custom one, fleetd configuration, fleet CA root)
require.Len(t, cmds, 4)
var installProfileCount, installEnterpriseCount, otherCount int
var profileCustomSeen, profileFleetdSeen, profileFleetCASeen, profileFileVaultSeen bool
for _, cmd := range cmds {
switch cmd.Command.RequestType {
case "InstallProfile":
installProfileCount++
if strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>") {
profileCustomSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)) {
profileFleetdSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)) {
profileFleetCASeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant") {
profileFileVaultSeen = true
}
case "InstallEnterpriseApplication":
installEnterpriseCount++
default:
otherCount++
}
}
require.Equal(t, 3, installProfileCount)
require.Equal(t, 1, installEnterpriseCount)
require.Equal(t, 0, otherCount)
require.True(t, profileCustomSeen)
require.True(t, profileFleetdSeen)
require.True(t, profileFleetCASeen)
require.False(t, profileFileVaultSeen)
// simulate fleetd being installed and the host being orbit-enrolled now
enrolledHost.OsqueryHostID = ptr.String(mdmDevice.UUID)
orbitKey := setOrbitEnrollment(t, enrolledHost, s.ds)
enrolledHost.OrbitNodeKey = &orbitKey
// there shouldn't be a worker Release Device pending job (we don't release that way anymore)
pending, err := s.ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute))
require.NoError(t, err)
require.Len(t, pending, 0)
// call the /status endpoint, the software and script should be pending
var statusResp getOrbitSetupExperienceStatusResponse
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved
require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved
require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile
var profNames []string
var profStatuses []fleet.MDMDeliveryStatus
for _, prof := range statusResp.Results.ConfigurationProfiles {
profNames = append(profNames, prof.Name)
profStatuses = append(profStatuses, prof.Status)
}
require.ElementsMatch(t, []string{"N1", "Fleetd configuration", "Fleet root certificate authority (CA)"}, profNames)
require.ElementsMatch(t, []fleet.MDMDeliveryStatus{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying}, profStatuses)
// the software and script are still pending
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
// no MDM command got enqueued due to the /status call (device not released yet)
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
require.Nil(t, cmd)
// TODO(mna): here we should call the "state machine" to trigger creation of
// the software install and script execution requests, but that is not
// implemented yet.
// TODO(mna): when callback of software/script results are implemented, this will
// automatically update the /status responses, and once everything has run it will
// automatically release the device.
// record a result for software installation
/*
var installResp orbitPostSoftwareInstallResultResponse
s.DoJSON("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"install_script_exit_code": 0,
"install_script_output": "ok"
}`, *enrolledHost.OrbitNodeKey, installUUID)), http.StatusOK, &installResp)
// status still shows script as pending
statusResp = getOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved
require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved
require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// no MDM command got enqueued due to the /status call (device not released yet)
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
require.Nil(t, cmd)
// record a result for script execution
var scriptResp orbitPostScriptResultResponse
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *enrolledHost.OrbitNodeKey, execID)),
http.StatusOK, &scriptResp)
// check that the host received the device configured command automatically
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
cmds = cmds[:0]
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 {
switch cmd.Command.RequestType {
case "DeviceConfigured":
deviceConfiguredCount++
default:
otherCount++
}
}
require.Equal(t, 1, deviceConfiguredCount)
require.Equal(t, 0, otherCount)
*/
}
func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptForceRelease() {
t := s.T()
ctx := context.Background()
teamDevice, enrolledHost, _ := s.createTeamDeviceForSetupExperienceWithProfileSoftwareAndScript()
// enroll the host
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
mdmDevice.SerialNumber = teamDevice.SerialNumber
err := mdmDevice.Enroll()
require.NoError(t, err)
// run the worker to process the DEP enroll request
s.runWorker()
// run the worker to assign configuration profiles
s.awaitTriggerProfileSchedule(t)
var cmds []*micromdm.CommandPayload
cmd, err := mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
var fullCmd micromdm.CommandPayload
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
// Can be useful for debugging
//switch cmd.Command.RequestType {
//case "InstallProfile":
// fmt.Println(">>>> device received command: ", cmd.CommandUUID, cmd.Command.RequestType, string(fullCmd.Command.InstallProfile.Payload))
//case "InstallEnterpriseApplication":
// 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)
// }
//default:
// fmt.Println(">>>> device received command: ", cmd.Command.RequestType)
//}
cmds = append(cmds, &fullCmd)
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
}
// expected commands: install fleetd (install enterprise), install profiles
// (custom one, fleetd configuration, fleet CA root)
require.Len(t, cmds, 4)
var installProfileCount, installEnterpriseCount, otherCount int
var profileCustomSeen, profileFleetdSeen, profileFleetCASeen, profileFileVaultSeen bool
for _, cmd := range cmds {
switch cmd.Command.RequestType {
case "InstallProfile":
installProfileCount++
if strings.Contains(string(cmd.Command.InstallProfile.Payload), "<string>I1</string>") {
profileCustomSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetdConfigPayloadIdentifier)) {
profileFleetdSeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string>", mobileconfig.FleetCARootConfigPayloadIdentifier)) {
profileFleetCASeen = true
} else if strings.Contains(string(cmd.Command.InstallProfile.Payload), fmt.Sprintf("<string>%s</string", mobileconfig.FleetFileVaultPayloadIdentifier)) &&
strings.Contains(string(cmd.Command.InstallProfile.Payload), "ForceEnableInSetupAssistant") {
profileFileVaultSeen = true
}
case "InstallEnterpriseApplication":
installEnterpriseCount++
default:
otherCount++
}
}
require.Equal(t, 3, installProfileCount)
require.Equal(t, 1, installEnterpriseCount)
require.Equal(t, 0, otherCount)
require.True(t, profileCustomSeen)
require.True(t, profileFleetdSeen)
require.True(t, profileFleetCASeen)
require.False(t, profileFileVaultSeen)
// simulate fleetd being installed and the host being orbit-enrolled now
enrolledHost.OsqueryHostID = ptr.String(mdmDevice.UUID)
orbitKey := setOrbitEnrollment(t, enrolledHost, s.ds)
enrolledHost.OrbitNodeKey = &orbitKey
// there shouldn't be a worker Release Device pending job (we don't release that way anymore)
pending, err := s.ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute))
require.NoError(t, err)
require.Len(t, pending, 0)
// call the /status endpoint, the software and script should be pending
var statusResp getOrbitSetupExperienceStatusResponse
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved
require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved
require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile
var profNames []string
var profStatuses []fleet.MDMDeliveryStatus
for _, prof := range statusResp.Results.ConfigurationProfiles {
profNames = append(profNames, prof.Name)
profStatuses = append(profStatuses, prof.Status)
}
require.ElementsMatch(t, []string{"N1", "Fleetd configuration", "Fleet root certificate authority (CA)"}, profNames)
require.ElementsMatch(t, []fleet.MDMDeliveryStatus{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying}, profStatuses)
// the software and script are still pending
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
// no MDM command got enqueued due to the /status call (device not released yet)
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
require.Nil(t, cmd)
// call the /status endpoint again but this time force the release
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "force_release": true}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
// the software and script are still pending
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 1)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
// check that the host received the device configured command even if
// software and script are still pending
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
cmds = cmds[:0]
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 {
switch cmd.Command.RequestType {
case "DeviceConfigured":
deviceConfiguredCount++
default:
otherCount++
}
}
require.Equal(t, 1, deviceConfiguredCount)
require.Equal(t, 0, otherCount)
}
+60
View File
@@ -6,11 +6,13 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
"sync"
"testing"
@@ -527,3 +529,61 @@ func (ts *withServer) lastActivityOfTypeDoesNotMatch(name, details string, id ui
}
}
}
func (ts *withServer) uploadSoftwareInstaller(
t *testing.T,
payload *fleet.UploadSoftwareInstallerPayload,
expectedStatus int,
expectedError string,
) {
t.Helper()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
f := openFile(payload.Filename)
defer f.Close()
payload.InstallerFile = f
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the software field
fw, err := w.CreateFormFile("software", payload.Filename)
require.NoError(t, err)
n, err := io.Copy(fw, payload.InstallerFile)
require.NoError(t, err)
require.NotZero(t, n)
// add the team_id field
if payload.TeamID != nil {
require.NoError(t, w.WriteField("team_id", fmt.Sprintf("%d", *payload.TeamID)))
}
// add the remaining fields
require.NoError(t, w.WriteField("install_script", payload.InstallScript))
require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery))
require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript))
require.NoError(t, w.WriteField("uninstall_script", payload.UninstallScript))
if payload.SelfService {
require.NoError(t, w.WriteField("self_service", "true"))
}
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
"Accept": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", ts.token),
}
r := ts.DoRawWithHeaders("POST", "/api/latest/fleet/software/package", b.Bytes(), expectedStatus, headers)
defer r.Body.Close()
if expectedError != "" {
errMsg := extractServerErrorText(r.Body)
require.Contains(t, errMsg, expectedError)
}
}