mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34433 Part 2 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. Added by first PR - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [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] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Profiles now install during device enrollment setup * **Bug Fixes** * Enhanced Apple MDM profile synchronization to handle concurrent processing scenarios * Improved profile reconciliation to prevent conflicts when multiple workers process the same device simultaneously <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
235 lines
8 KiB
Go
235 lines
8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func (s *integrationMDMTestSuite) TestReleaseWorker() {
|
|
t := s.T()
|
|
ctx := context.Background()
|
|
mysql.TruncateTables(t, s.ds, "nano_commands", "host_mdm_apple_profiles", "mdm_apple_configuration_profiles") // We truncate this table beforehand to avoid persistence from other tests.
|
|
|
|
type mdmCommandOfType struct {
|
|
CommandType string
|
|
Count int
|
|
}
|
|
expectMDMCommandsOfType := func(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, commandTypes []mdmCommandOfType) {
|
|
// Get the first command
|
|
cmd, err := mdmDevice.Idle()
|
|
|
|
for _, ct := range commandTypes {
|
|
commandType := ct.CommandType
|
|
count := ct.Count
|
|
// Acknowledge and get next command of the expected type, for the expected count
|
|
// of times.
|
|
|
|
for range count {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, commandType, cmd.Command.RequestType)
|
|
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
|
}
|
|
}
|
|
|
|
// We do not expect any other commands
|
|
require.Nil(t, cmd)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
expectDeviceConfiguredSent := func(t *testing.T, shouldBeSent bool) {
|
|
expectedCount := 0
|
|
if shouldBeSent == true {
|
|
expectedCount = 1
|
|
}
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var count int
|
|
err := sqlx.GetContext(ctx, q, &count, "SELECT COUNT(*) FROM nano_commands WHERE request_type = 'DeviceConfigured'")
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, expectedCount, count)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Helper function to set the queued job not_before to current time to ensure it can be picked up without waiting.
|
|
speedUpQueuedAppleMdmJob := func(t *testing.T) {
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, "UPDATE jobs SET not_before = ? WHERE name = 'apple_mdm' AND state = 'queued'", time.Now().Add(-1*time.Second))
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
enrollAppleDevice := func(t *testing.T, device godep.Device) *mdmtest.TestAppleMDMClient {
|
|
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
|
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
|
mdmDevice.SerialNumber = device.SerialNumber
|
|
err := mdmDevice.Enroll()
|
|
require.NoError(t, err)
|
|
|
|
cmd, err := mdmDevice.Idle()
|
|
require.Nil(t, cmd) // check no command is enqueued.
|
|
require.NoError(t, err)
|
|
|
|
return mdmDevice
|
|
}
|
|
|
|
device := godep.Device{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"}
|
|
|
|
profileAssignmentReqs := []profileAssignmentReq{}
|
|
s.setSkipWorkerJobs(t)
|
|
s.enableABM(t.Name())
|
|
s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
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":
|
|
// This endpoint is used to get an initial list of
|
|
// devices, return a single device
|
|
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))
|
|
profileAssignmentReqs = append(profileAssignmentReqs, 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(`{}`))
|
|
}
|
|
}))
|
|
|
|
// query all hosts
|
|
listHostsRes := listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Empty(t, listHostsRes.Hosts) // no hosts yet
|
|
|
|
// trigger a profile sync
|
|
s.runDEPSchedule()
|
|
|
|
// all devices should be returned from the hosts endpoint
|
|
listHostsRes = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
|
require.Len(t, listHostsRes.Hosts, 1)
|
|
|
|
t.Run("automatic release", func(t *testing.T) {
|
|
t.Run("waits for config profiles being installed", func(t *testing.T) {
|
|
// Clean up
|
|
mysql.TruncateTables(t, s.ds, "mdm_apple_configuration_profiles", "host_mdm_apple_profiles", "nano_commands") // Clean tables after use
|
|
// Simulate profile reconciler running at least once before enrollment, and adds fleet profiles to the team
|
|
s.awaitTriggerProfileSchedule(t)
|
|
config := mobileconfigForTest("N1", "I1")
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{config}}, http.StatusNoContent)
|
|
|
|
// enroll the host
|
|
mdmDevice := enrollAppleDevice(t, device)
|
|
|
|
// Run worker to start device release (NOTE: Should not release yet)
|
|
s.awaitRunAppleMDMWorkerSchedule()
|
|
speedUpQueuedAppleMdmJob(t)
|
|
|
|
// Get install enterprise application command and acknowledge it
|
|
expectMDMCommandsOfType(t, mdmDevice, []mdmCommandOfType{
|
|
{
|
|
CommandType: "InstallEnterpriseApplication",
|
|
Count: 1,
|
|
},
|
|
{
|
|
CommandType: "InstallProfile",
|
|
Count: 3,
|
|
},
|
|
{
|
|
CommandType: "DeclarativeManagement",
|
|
Count: 1,
|
|
},
|
|
})
|
|
|
|
s.awaitRunAppleMDMWorkerSchedule() // Run after install enterprise command to install profiles. (Should requeue until we trigger profile schedule)
|
|
|
|
s.awaitRunAppleMDMWorkerSchedule() // release device
|
|
// Since moving profile installation to POSTDepEnrollment worker, we can now release the device immediately, as we only wait for sending.
|
|
// Verify device was released
|
|
expectDeviceConfiguredSent(t, true)
|
|
expectMDMCommandsOfType(t, mdmDevice, []mdmCommandOfType{
|
|
{
|
|
CommandType: "DeviceConfigured",
|
|
Count: 1,
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("ignores user scoped config profiles", func(t *testing.T) {
|
|
mysql.TruncateTables(t, s.ds, "mdm_apple_configuration_profiles", "host_mdm_apple_profiles", "nano_commands") // Clean tables after use
|
|
// Simulate profile reconciler running at least once before enrollment, and adds fleet profiles to the team
|
|
s.awaitTriggerProfileSchedule(t)
|
|
systemScopedConfig := mobileconfigForTest("N1", "I1")
|
|
userScope := fleet.PayloadScopeUser
|
|
userScopedConfig := scopedMobileconfigForTest("N-USER-SCOPED", "I-USER-SCOPED", &userScope)
|
|
s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{systemScopedConfig, userScopedConfig}}, http.StatusNoContent)
|
|
|
|
// enroll the host
|
|
mdmDevice := enrollAppleDevice(t, device)
|
|
|
|
// Run worker to start device release (NOTE: Should not release yet)
|
|
s.awaitRunAppleMDMWorkerSchedule()
|
|
speedUpQueuedAppleMdmJob(t)
|
|
|
|
expectMDMCommandsOfType(t, mdmDevice, []mdmCommandOfType{
|
|
{
|
|
CommandType: "InstallEnterpriseApplication",
|
|
Count: 1,
|
|
},
|
|
{
|
|
CommandType: "InstallProfile",
|
|
Count: 3, // Only the system scoped profile is installed
|
|
},
|
|
{
|
|
CommandType: "DeclarativeManagement",
|
|
Count: 1,
|
|
},
|
|
})
|
|
|
|
s.awaitRunAppleMDMWorkerSchedule() // Run after post dep enrollment to release device.
|
|
|
|
// Verify device was not released yet
|
|
expectDeviceConfiguredSent(t, true)
|
|
expectMDMCommandsOfType(t, mdmDevice, []mdmCommandOfType{
|
|
{
|
|
CommandType: "DeviceConfigured",
|
|
Count: 1,
|
|
},
|
|
})
|
|
})
|
|
})
|
|
}
|