mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34433 It speeds up the cron, meaning fleetd, bootstrap and now profiles should be sent within 10 seconds of being known to fleet, compared to the previous 1 minute. It's heavily based on my last PR, so the structure and changes are close to identical, with some small differences. **I did not do the redis key part in this PR, as I think that should come in it's own PR, to avoid overlooking logic bugs with that code, and since this one is already quite sized since we're moving core pieces of code around.** # 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. ## 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** * Faster macOS onboarding: device profiles are delivered and installed as part of DEP enrollment, shortening initial setup. * Improved profile handling: per-host profile preprocessing, secret detection, and clearer failure marking. * **Improvements** * Consolidated SCEP/NDES error messaging for clearer diagnostics. * Cron/work scheduling tuned to prioritize Apple MDM profile delivery. * **Tests** * Expanded MDM unit and integration tests, including DeclarativeManagement handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1544 lines
52 KiB
Go
1544 lines
52 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
|
|
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"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/mobileconfig"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
|
|
"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/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
|
"github.com/micromdm/plist"
|
|
"github.com/smallstep/pkcs7"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// NOTE: the mantra for lifecycle events is:
|
|
// - Noah: When MDM is turned on, install fleetd, bootstrap package (if DEP),
|
|
// and profiles. Don't clear host vitals (everything you see on the Host
|
|
// details page)
|
|
// - Noah: On re-enrollment, don't clear host vitals.
|
|
// - Noah: On lock and wipe, don't clear host vitals.
|
|
// - Noah: On delete, clear host vitals.
|
|
|
|
// NOTE: ADE lifecycle events are part of the integration_mdm_dep_test.go file
|
|
|
|
type mdmLifecycleAssertion[T any] func(t *testing.T, host *fleet.Host, device T)
|
|
|
|
func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() {
|
|
t := s.T()
|
|
s.setSkipWorkerJobs(t)
|
|
s.setupLifecycleSettings()
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
Action mdmLifecycleAssertion[*mdmtest.TestAppleMDMClient]
|
|
}{
|
|
{
|
|
"wiped host turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
s.Do(
|
|
"POST",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
)
|
|
|
|
cmd, err := device.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
cmd, err = device.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"locked host turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
s.Do(
|
|
"POST",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
)
|
|
|
|
cmd, err := device.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
cmd, err = device.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"host turns on MDM features out of the blue",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"IT admin turns off MDM for a host via the UI then host turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
originalPushMock := s.pushProvider.PushFunc
|
|
defer func() { s.pushProvider.PushFunc = originalPushMock }()
|
|
|
|
s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
|
|
res, err := mockSuccessfulPush(ctx, pushes)
|
|
require.NoError(t, err)
|
|
err = device.Checkout()
|
|
require.NoError(t, err)
|
|
return res, err
|
|
}
|
|
|
|
s.Do(
|
|
"DELETE",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
|
|
nil,
|
|
http.StatusNoContent,
|
|
)
|
|
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"host is deleted then turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
var delResp deleteHostResponse
|
|
s.DoJSON(
|
|
"DELETE",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
&delResp,
|
|
)
|
|
|
|
dupeClient := mdmtest.NewTestMDMClientAppleDirect(
|
|
mdmtest.AppleEnrollInfo{
|
|
SCEPChallenge: s.scepChallenge,
|
|
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
|
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
|
}, "MacBookPro16,1")
|
|
dupeClient.UUID = device.UUID
|
|
dupeClient.SerialNumber = device.SerialNumber
|
|
dupeClient.Model = device.Model
|
|
require.NoError(t, dupeClient.Enroll())
|
|
|
|
*device = *dupeClient
|
|
},
|
|
},
|
|
{
|
|
"host is deleted in bulk then turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
req := deleteHostsRequest{
|
|
IDs: []uint{host.ID},
|
|
}
|
|
resp := deleteHostsResponse{}
|
|
s.DoJSON("POST", "/api/latest/fleet/hosts/delete", req, http.StatusOK, &resp)
|
|
|
|
dupeClient := mdmtest.NewTestMDMClientAppleDirect(
|
|
mdmtest.AppleEnrollInfo{
|
|
SCEPChallenge: s.scepChallenge,
|
|
SCEPURL: s.server.URL + apple_mdm.SCEPPath,
|
|
MDMURL: s.server.URL + apple_mdm.MDMPath,
|
|
}, "MacBookPro16,1")
|
|
dupeClient.UUID = device.UUID
|
|
dupeClient.SerialNumber = device.SerialNumber
|
|
dupeClient.Model = device.Model
|
|
require.NoError(t, dupeClient.Enroll())
|
|
|
|
*device = *dupeClient
|
|
},
|
|
},
|
|
{
|
|
"host is deleted then osquery enrolls then turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
|
|
var delResp deleteHostResponse
|
|
s.DoJSON(
|
|
"DELETE",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
&delResp,
|
|
)
|
|
|
|
var err error
|
|
host.OsqueryHostID = ptr.String(t.Name())
|
|
host, err = s.ds.NewHost(context.Background(), host)
|
|
require.NoError(t, err)
|
|
|
|
setOrbitEnrollment(t, host, s.ds)
|
|
deviceToken := uuid.NewString()
|
|
err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, deviceToken)
|
|
require.NoError(t, err)
|
|
|
|
device.SetDesktopToken(deviceToken)
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
}
|
|
|
|
assertAction := func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient, action mdmLifecycleAssertion[*mdmtest.TestAppleMDMClient]) {
|
|
fCmds, fSumm, fHostMDM := s.recordAppleHostStatus(host, device)
|
|
|
|
action(t, host, device)
|
|
|
|
// reload the host by identifier, tests might
|
|
// delete hosts and create new records with different IDs
|
|
var err error
|
|
host, err = s.ds.HostByIdentifier(context.Background(), host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
sCmds, sSumm, sHostMDM := s.recordAppleHostStatus(host, device)
|
|
|
|
// post asssertions
|
|
require.ElementsMatch(t, fCmds, sCmds)
|
|
require.Equal(t, fSumm, sSumm)
|
|
require.Equal(t, fHostMDM, sHostMDM)
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.Name, func(t *testing.T) {
|
|
t.Run("manual enrollment", func(t *testing.T) {
|
|
host, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
|
assertAction(t, host, device, tt.Action)
|
|
})
|
|
|
|
t.Run("automatic enrollment", func(t *testing.T) {
|
|
device := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, "")
|
|
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":
|
|
_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
|
|
case "/profile":
|
|
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
|
|
require.NoError(t, err)
|
|
case "/profile/devices":
|
|
err := encoder.Encode(godep.ProfileResponse{
|
|
ProfileUUID: "abc",
|
|
Devices: map[string]string{},
|
|
})
|
|
require.NoError(t, err)
|
|
case "/server/devices", "/devices/sync":
|
|
err := encoder.Encode(godep.DeviceResponse{
|
|
Devices: []godep.Device{
|
|
{
|
|
SerialNumber: device.SerialNumber,
|
|
Model: device.Model,
|
|
OS: "osx",
|
|
OpType: "added",
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
}))
|
|
|
|
s.runDEPSchedule()
|
|
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
|
device.SetDEPToken(depURLToken)
|
|
var err error
|
|
host, err := s.ds.HostByIdentifier(context.Background(), device.SerialNumber)
|
|
require.NoError(t, err)
|
|
require.NoError(t, device.Enroll())
|
|
|
|
assertAction(t, host, device, tt.Action)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsWindows() {
|
|
t := s.T()
|
|
s.setupLifecycleSettings()
|
|
|
|
testCases := []struct {
|
|
Name string
|
|
Action mdmLifecycleAssertion[*mdmtest.TestWindowsMDMClient]
|
|
}{
|
|
{
|
|
"wiped host turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
|
|
s.Do(
|
|
"POST",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID),
|
|
json.RawMessage(`{ "windows": {"wipe_type": "doWipe"}}`),
|
|
http.StatusOK,
|
|
)
|
|
|
|
status, err := s.ds.GetHostLockWipeStatus(context.Background(), host)
|
|
require.NoError(t, err)
|
|
|
|
cmds, err := device.StartManagementSession()
|
|
require.NoError(t, err)
|
|
|
|
// two status + the wipe command we enqueued
|
|
require.Len(t, cmds, 3)
|
|
wipeCmd := cmds[status.WipeMDMCommand.CommandUUID]
|
|
require.NotNil(t, wipeCmd)
|
|
require.Equal(t, wipeCmd.Verb, fleet.CmdExec)
|
|
require.Len(t, wipeCmd.Cmd.Items, 1)
|
|
require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipe", *wipeCmd.Cmd.Items[0].Target)
|
|
|
|
msgID, err := device.GetCurrentMsgID()
|
|
require.NoError(t, err)
|
|
|
|
device.AppendResponse(fleet.SyncMLCmd{
|
|
XMLName: xml.Name{Local: fleet.CmdStatus},
|
|
MsgRef: &msgID,
|
|
CmdRef: &status.WipeMDMCommand.CommandUUID,
|
|
Cmd: ptr.String("Exec"),
|
|
Data: ptr.String("200"),
|
|
Items: nil,
|
|
CmdID: fleet.CmdID{Value: uuid.NewString()},
|
|
})
|
|
cmds, err = device.SendResponse()
|
|
require.NoError(t, err)
|
|
// the ack of the message should be the only returned command
|
|
require.Len(t, cmds, 1)
|
|
|
|
// Simulate the host having fleetd installed and reporting back in as un-enrolled
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(), `
|
|
UPDATE host_mdm
|
|
SET enrolled = 0, server_url = ''
|
|
WHERE host_id = ?
|
|
`, host.ID)
|
|
return err
|
|
})
|
|
|
|
// re-enroll
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"locked host turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
|
|
s.Do(
|
|
"POST",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
)
|
|
|
|
status, err := s.ds.GetHostLockWipeStatus(context.Background(), host)
|
|
require.NoError(t, err)
|
|
|
|
var orbitScriptResp 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"}`,
|
|
*host.OrbitNodeKey,
|
|
status.LockScript.ExecutionID,
|
|
),
|
|
),
|
|
http.StatusOK,
|
|
&orbitScriptResp,
|
|
)
|
|
|
|
// Simulate the host having fleetd installed after being wiped and reporting back in as un-enrolled
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(), `
|
|
UPDATE host_mdm
|
|
SET enrolled = 0, server_url = ''
|
|
WHERE host_id = ?
|
|
`, host.ID)
|
|
return err
|
|
})
|
|
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
{
|
|
"host turns on MDM features out of the blue",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
|
|
if strings.Contains(t.Name(), "automatic") {
|
|
require.NoError(t, device.Enroll())
|
|
} else {
|
|
// A programatically-enrolled host that randomly turns on MDM after already enabled will get a SOAP fault
|
|
require.Error(t, device.Enroll())
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"host is deleted then osquery enrolls then turns on MDM",
|
|
func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient) {
|
|
var delResp deleteHostResponse
|
|
s.DoJSON(
|
|
"DELETE",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
&delResp,
|
|
)
|
|
|
|
var err error
|
|
host.OsqueryHostID = ptr.String(t.Name())
|
|
host, err = s.ds.NewHost(context.Background(), host)
|
|
require.NoError(t, err)
|
|
|
|
orbitKey := setOrbitEnrollment(t, host, s.ds)
|
|
host.OrbitNodeKey = &orbitKey
|
|
if !strings.Contains(device.TokenIdentifier, "@") {
|
|
device.TokenIdentifier = orbitKey
|
|
}
|
|
device.HardwareID = host.UUID
|
|
device.DeviceID = host.UUID
|
|
|
|
require.NoError(t, device.Enroll())
|
|
},
|
|
},
|
|
}
|
|
|
|
assertAction := func(t *testing.T, host *fleet.Host, device *mdmtest.TestWindowsMDMClient, action mdmLifecycleAssertion[*mdmtest.TestWindowsMDMClient]) {
|
|
fCmds, fSumm, fHostMDM := s.recordWindowsHostStatus(host, device)
|
|
|
|
action(t, host, device)
|
|
|
|
// reload the host by identifier, tests might
|
|
// delete hosts and create new records with different IDs
|
|
var err error
|
|
host, err = s.ds.HostByIdentifier(context.Background(), host.UUID)
|
|
require.NoError(t, err)
|
|
|
|
sCmds, sSumm, sHostMDM := s.recordWindowsHostStatus(host, device)
|
|
|
|
// post asssertions
|
|
require.Len(t, sCmds, len(fCmds))
|
|
require.ElementsMatch(t, fCmds, sCmds)
|
|
require.Equal(t, fSumm, sSumm)
|
|
require.Equal(t, fHostMDM, sHostMDM)
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.Name, func(t *testing.T) {
|
|
t.Run("programmatic enrollment", func(t *testing.T) {
|
|
host, device := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
|
|
err := s.ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
assertAction(t, host, device, tt.Action)
|
|
})
|
|
|
|
t.Run("automatic enrollment", func(t *testing.T) {
|
|
if strings.Contains(tt.Name, "wipe") {
|
|
t.Skip("wipe tests are not supported for windows automatic enrollment until we fix #TODO")
|
|
}
|
|
|
|
tenantID := uuid.New().String()
|
|
|
|
acResp := appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "windows_entra_tenant_ids": ["`+tenantID+`"] } }`), http.StatusOK, &acResp)
|
|
|
|
err := s.ds.ApplyEnrollSecrets(context.Background(), nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
|
|
require.NoError(t, err)
|
|
|
|
host := createOrbitEnrolledHost(t, "windows", "windows_automatic", s.ds)
|
|
|
|
azureMail := "foo.bar.baz@example.com"
|
|
device := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail, mdmtest.TestWindowsMDMClientWithSigningKeyAndTenantID(s.jwtSigningKey, defaultFakeJWTKeyID, tenantID))
|
|
device.HardwareID = host.UUID
|
|
device.DeviceID = host.UUID
|
|
require.NoError(t, device.Enroll())
|
|
|
|
err = s.ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
|
|
assertAction(t, host, device, tt.Action)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// Hardcode response type because we are using a custom json marshaling so
|
|
// using getHostMDMResponse fails with "JSON unmarshaling is not supported for HostMDM".
|
|
type jsonMDM struct {
|
|
EnrollmentStatus string `json:"enrollment_status"`
|
|
ServerURL string `json:"server_url"`
|
|
Name string `json:"name,omitempty"`
|
|
ID *uint `json:"id,omitempty"`
|
|
}
|
|
type getHostMDMResponseTest struct {
|
|
HostMDM *jsonMDM
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) recordWindowsHostStatus(
|
|
host *fleet.Host,
|
|
device *mdmtest.TestWindowsMDMClient,
|
|
) ([]fleet.ProtoCmdOperation, getHostMDMSummaryResponse, getHostMDMResponseTest) {
|
|
t := s.T()
|
|
|
|
var recordedCmds []fleet.ProtoCmdOperation
|
|
cmds, err := device.StartManagementSession()
|
|
require.NoError(t, err)
|
|
|
|
msgID, err := device.GetCurrentMsgID()
|
|
require.NoError(t, err)
|
|
for _, c := range cmds {
|
|
cmdID := c.Cmd.CmdID
|
|
status := syncml.CmdStatusOK
|
|
device.AppendResponse(fleet.SyncMLCmd{
|
|
XMLName: xml.Name{Local: fleet.CmdStatus},
|
|
MsgRef: &msgID,
|
|
CmdRef: &cmdID.Value,
|
|
Cmd: ptr.String(c.Verb),
|
|
Data: &status,
|
|
Items: nil,
|
|
CmdID: fleet.CmdID{Value: uuid.NewString()},
|
|
})
|
|
c.Cmd.CmdID.Value = ""
|
|
c.Cmd.CmdRef = nil
|
|
recordedCmds = append(recordedCmds, c)
|
|
}
|
|
|
|
_, err = device.SendResponse()
|
|
require.NoError(t, err)
|
|
|
|
mdmAgg := getHostMDMSummaryResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/summary/mdm", nil, http.StatusOK, &mdmAgg)
|
|
|
|
ghr := getHostMDMResponseTest{}
|
|
s.DoJSON(
|
|
"GET",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
&ghr,
|
|
)
|
|
|
|
return recordedCmds, mdmAgg, ghr
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) recordAppleHostStatus(
|
|
host *fleet.Host,
|
|
device *mdmtest.TestAppleMDMClient,
|
|
) ([]*micromdm.CommandPayload, getHostMDMSummaryResponse, getHostMDMResponseTest) {
|
|
t := s.T()
|
|
|
|
s.runWorkerUntilDone()
|
|
s.awaitTriggerProfileSchedule(t)
|
|
|
|
var cmds []*micromdm.CommandPayload
|
|
|
|
cmd, err := device.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
|
|
// command uuid is a random value, we only care that's set
|
|
require.NotEmpty(t, cmd.CommandUUID)
|
|
|
|
// strip the signature of the profiles so they can be easily compared
|
|
if cmd.Command.RequestType == "InstallProfile" {
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
fullCmd.CommandUUID = ""
|
|
p7, err := pkcs7.Parse(fullCmd.Command.InstallProfile.Payload)
|
|
require.NoError(t, err)
|
|
fullCmd.Command.InstallProfile.Payload = p7.Content
|
|
}
|
|
cmds = append(cmds, &fullCmd)
|
|
|
|
cmd, err = device.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
mdmAgg := getHostMDMSummaryResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/summary/mdm", nil, http.StatusOK, &mdmAgg)
|
|
|
|
ghr := getHostMDMResponseTest{}
|
|
s.DoJSON(
|
|
"GET",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID),
|
|
nil,
|
|
http.StatusOK,
|
|
&ghr,
|
|
)
|
|
|
|
return cmds, mdmAgg, ghr
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) setupLifecycleSettings() {
|
|
t := s.T()
|
|
ctx := context.Background()
|
|
// add bootstrap package
|
|
_ = s.ds.DeleteMDMAppleBootstrapPackage(ctx, 0)
|
|
bp, err := os.ReadFile(filepath.Join("testdata", "bootstrap-packages", "signed.pkg"))
|
|
require.NoError(t, err)
|
|
s.uploadBootstrapPackage(
|
|
&fleet.MDMAppleBootstrapPackage{Bytes: bp, Name: "pkg.pkg", TeamID: 0},
|
|
http.StatusOK,
|
|
"",
|
|
false,
|
|
)
|
|
|
|
// enable disk encryption
|
|
acResp := appConfigResponse{}
|
|
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
|
|
"mdm": { "macos_settings": {"enable_disk_encryption": true} }
|
|
}`), http.StatusOK, &acResp)
|
|
require.True(t, acResp.MDM.EnableDiskEncryption.Value)
|
|
|
|
// add profiles (windows, mac)
|
|
s.Do(
|
|
"POST",
|
|
"/api/v1/fleet/mdm/profiles/batch",
|
|
batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
|
{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
|
|
{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
|
|
{Name: "N3", Contents: declarationForTest("D1")},
|
|
}},
|
|
http.StatusNoContent,
|
|
)
|
|
}
|
|
|
|
// Host is renewing SCEP certificates
|
|
func (s *integrationMDMTestSuite) TestLifecycleSCEPCertExpiration() {
|
|
t := s.T()
|
|
ctx := context.Background()
|
|
s.setSkipWorkerJobs(t)
|
|
|
|
// helper functions
|
|
getEnrollRef := func(hostUUID string) string {
|
|
var foundRef string
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &foundRef, `
|
|
SELECT fleet_enroll_ref FROM host_mdm
|
|
WHERE host_id = (SELECT id FROM hosts WHERE uuid = ?)
|
|
`, hostUUID)
|
|
})
|
|
return foundRef
|
|
}
|
|
|
|
existsRefetchCmd := func(hostUUID string) bool {
|
|
var foundRefetchCmd bool
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &foundRefetchCmd, `
|
|
SELECT 1 FROM host_mdm_commands
|
|
WHERE host_id = (SELECT id FROM hosts WHERE uuid = ?)
|
|
AND command_type = 'REFETCH-DEVICE-'
|
|
`, hostUUID)
|
|
})
|
|
return foundRefetchCmd
|
|
}
|
|
|
|
getRenewCmdUUID := func(hostUUID string) sql.NullString {
|
|
var renewCmdUUID sql.NullString
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &renewCmdUUID, `
|
|
SELECT renew_command_uuid FROM nano_cert_auth_associations WHERE id = ?
|
|
`, hostUUID)
|
|
})
|
|
return renewCmdUUID
|
|
}
|
|
|
|
reportResultsRefetchCmds := func(device *mdmtest.TestAppleMDMClient) {
|
|
var gotRefetchCmd *micromdm.CommandPayload
|
|
gotCmdTypes := []string{}
|
|
cmd, err := device.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
gotCmdTypes = append(gotCmdTypes, cmd.Command.RequestType)
|
|
|
|
switch fullCmd.Command.RequestType {
|
|
case "DeviceInformation":
|
|
gotRefetchCmd = &fullCmd
|
|
// respond with some basic info
|
|
cmd, err = device.AcknowledgeDeviceInformation(device.UUID, cmd.CommandUUID, "Test iPad", "iPad Pro", "America/Los_Angeles")
|
|
require.NoError(t, err)
|
|
continue
|
|
case "InstalledApplicationList":
|
|
// respond with empty list
|
|
cmd, err = device.AcknowledgeInstalledApplicationList(device.UUID, cmd.CommandUUID, []fleet.Software{})
|
|
require.NoError(t, err)
|
|
continue
|
|
case "CertificateList":
|
|
// respond with empty list
|
|
cmd, err = device.AcknowledgeCertificateList(device.UUID, cmd.CommandUUID, []*x509.Certificate{})
|
|
require.NoError(t, err)
|
|
continue
|
|
default:
|
|
t.Fatalf("unexpected command: %s", fullCmd.Command.RequestType)
|
|
}
|
|
}
|
|
require.Len(t, gotCmdTypes, 3) // expect DeviceInformation, InstalledApplicationList, CertificateList
|
|
require.Contains(t, gotCmdTypes, "DeviceInformation")
|
|
require.NotNil(t, gotRefetchCmd)
|
|
}
|
|
|
|
// grab global enroll secrets for later
|
|
enrollSecrets, err := s.ds.GetEnrollSecrets(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, enrollSecrets)
|
|
|
|
// ensure there's a token for automatic enrollments
|
|
s.enableABM(t.Name())
|
|
|
|
// for our tests, we'll crete two ABM devices and some manual ones
|
|
devices := []godep.Device{
|
|
{SerialNumber: "serial-1", Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
|
{SerialNumber: "serial-2", Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
|
}
|
|
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: devices})
|
|
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: devices, 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(`{}`))
|
|
}
|
|
}))
|
|
s.runDEPSchedule()
|
|
|
|
// pending DEP hosts now exist
|
|
hostsBySerial := make(map[string]fleet.Host)
|
|
for _, device := range devices {
|
|
host, err := s.ds.HostByIdentifier(context.Background(), device.SerialNumber)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, host)
|
|
hostsBySerial[device.SerialNumber] = *host
|
|
}
|
|
|
|
// add a valid bootstrap package
|
|
b, err := os.ReadFile(filepath.Join("testdata", "bootstrap-packages", "signed.pkg"))
|
|
require.NoError(t, err)
|
|
signedPkg := b
|
|
s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "bs.pkg", TeamID: 0}, http.StatusOK, "", false)
|
|
|
|
// add a device that's manually enrolled
|
|
desktopToken := uuid.New().String()
|
|
manualHost := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
|
|
err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), manualHost.ID, desktopToken)
|
|
require.NoError(t, err)
|
|
manualEnrolledDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
|
|
manualEnrolledDevice.UUID = manualHost.UUID
|
|
manualEnrolledDevice.SerialNumber = manualHost.HardwareSerial
|
|
err = manualEnrolledDevice.Enroll()
|
|
require.NoError(t, err)
|
|
|
|
// add devices that are automatically enrolled
|
|
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
|
automaticEnrolledDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
|
automaticEnrolledDevice.SerialNumber = devices[0].SerialNumber
|
|
require.NoError(t, automaticEnrolledDevice.Enroll())
|
|
|
|
// add a device that's automatically enrolled with a server ref
|
|
automaticEnrolledDeviceWithRef := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
|
automaticEnrolledDeviceWithRef.SerialNumber = devices[1].SerialNumber
|
|
require.NoError(t, automaticEnrolledDeviceWithRef.Enroll())
|
|
require.NoError(
|
|
t,
|
|
s.ds.SetOrUpdateMDMData(
|
|
ctx,
|
|
hostsBySerial[devices[1].SerialNumber].ID,
|
|
false,
|
|
true,
|
|
s.server.URL,
|
|
true,
|
|
fleet.WellKnownMDMFleet,
|
|
"foo",
|
|
false,
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// add a device that was migrated from a third party mdm via
|
|
// "touchless" migration
|
|
migratedHost := createOrbitEnrolledHost(t, "darwin", "h4", s.ds)
|
|
migratedDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
|
|
migratedDevice.UUID = migratedHost.UUID
|
|
migratedDevice.SerialNumber = migratedHost.HardwareSerial
|
|
err = migratedDevice.Enroll()
|
|
require.NoError(t, err)
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
UPDATE nano_enrollments
|
|
SET enrolled_from_migration = 1
|
|
WHERE id = ?
|
|
`, migratedDevice.UUID)
|
|
return err
|
|
})
|
|
|
|
// Add an account driven user enrollment device
|
|
iPhoneHwModel := "iPhone14,2"
|
|
iphoneUser := &fleet.MDMIdPAccount{
|
|
Email: "iphone_user@example.com",
|
|
Fullname: "iPhone User",
|
|
Username: "iphone_user@example.com",
|
|
}
|
|
err = s.ds.InsertMDMIdPAccount(ctx, iphoneUser)
|
|
require.NoError(t, err)
|
|
iphoneUser, err = s.ds.GetMDMIdPAccountByEmail(ctx, iphoneUser.Email)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, iphoneUser)
|
|
require.Equal(t, iphoneUser.Email, "iphone_user@example.com")
|
|
|
|
iPhoneMdmDevice := mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
|
|
s.server.URL,
|
|
iPhoneHwModel,
|
|
iphoneUser.UUID,
|
|
)
|
|
require.NoError(t, iPhoneMdmDevice.Enroll())
|
|
assert.Equal(t, iPhoneMdmDevice.EnrollInfo.AssignedManagedAppleID, iphoneUser.Email)
|
|
|
|
// add global profiles
|
|
globalProfiles := [][]byte{
|
|
mobileconfigForTest("N1", "I1"),
|
|
mobileconfigForTest("N2", "I2"),
|
|
}
|
|
s.Do(
|
|
"POST",
|
|
"/api/v1/fleet/mdm/apple/profiles/batch",
|
|
batchSetMDMAppleProfilesRequest{Profiles: globalProfiles},
|
|
http.StatusNoContent,
|
|
)
|
|
expectedProfiles := 4 // Fleetd configuration, Fleet root cert, N1, N2
|
|
|
|
s.runWorker()
|
|
s.awaitTriggerProfileSchedule(t)
|
|
|
|
ackAllCommands := func(mdmDevice *mdmtest.TestAppleMDMClient, wantFleetdInstall, wantBootstrapInstall bool) int {
|
|
var count int
|
|
var foundFleetdInstall, foundBootstrapInstall bool
|
|
cmd, err := mdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
if cmd.Command.RequestType == "DeclarativeManagement" {
|
|
// skip declarative management commands
|
|
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
continue
|
|
}
|
|
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
count++
|
|
|
|
switch cmd.Command.RequestType {
|
|
case "InstallEnterpriseApplication":
|
|
if murl := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; murl != nil {
|
|
require.Contains(t, *murl, fleetdbase.GetPKGManifestURL())
|
|
foundFleetdInstall = true
|
|
} else {
|
|
manifest := fullCmd.Command.InstallEnterpriseApplication.Manifest
|
|
require.NotNil(t, manifest)
|
|
require.Len(t, manifest.ManifestItems, 1)
|
|
require.Len(t, manifest.ManifestItems[0].Assets, 1)
|
|
require.Contains(t, manifest.ManifestItems[0].Assets[0].URL, "fleet/mdm/bootstrap")
|
|
foundBootstrapInstall = true
|
|
}
|
|
case "InstallProfile":
|
|
// ok
|
|
default:
|
|
t.Errorf("unexpected command: %s", cmd.Command.RequestType)
|
|
}
|
|
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Equal(t, wantFleetdInstall, foundFleetdInstall)
|
|
require.Equal(t, wantBootstrapInstall, foundBootstrapInstall)
|
|
|
|
return count
|
|
}
|
|
|
|
// ack all commands to install profiles
|
|
require.Equal(t, expectedProfiles+1, ackAllCommands(manualEnrolledDevice, true, false))
|
|
require.Equal(t, expectedProfiles+2, ackAllCommands(automaticEnrolledDevice, true, true))
|
|
require.Equal(t, expectedProfiles+2, ackAllCommands(automaticEnrolledDeviceWithRef, true, true))
|
|
require.Equal(t, expectedProfiles+1, ackAllCommands(migratedDevice, true, false))
|
|
require.Equal(t, expectedProfiles-1, ackAllCommands(iPhoneMdmDevice, false, false)) // one less profile because no iOS means no fleetd
|
|
|
|
// simulate a device with two certificates by re-enrolling one of them
|
|
err = manualEnrolledDevice.Enroll()
|
|
require.NoError(t, err)
|
|
|
|
s.runWorker()
|
|
s.awaitTriggerProfileSchedule(t)
|
|
require.Equal(t, expectedProfiles+1, ackAllCommands(manualEnrolledDevice, true, false)) // re-enrolled device gets the same commands as before
|
|
require.Equal(t, 0, ackAllCommands(automaticEnrolledDevice, false, false))
|
|
require.Equal(t, 0, ackAllCommands(automaticEnrolledDeviceWithRef, false, false))
|
|
require.Equal(t, 0, ackAllCommands(migratedDevice, false, false))
|
|
|
|
cert, key, err := generateCertWithAPNsTopic()
|
|
require.NoError(t, err)
|
|
fleetCfg := config.TestConfig()
|
|
config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, "")
|
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
|
|
// run without expired certs, no command enqueued
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
cmd, err := manualEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDeviceWithRef.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = migratedDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = iPhoneMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
expireCerts := func() {
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
UPDATE nano_cert_auth_associations
|
|
SET cert_not_valid_after = DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
|
|
WHERE id IN (?, ?, ?, ?, ?)
|
|
`, manualHost.UUID, automaticEnrolledDevice.UUID, automaticEnrolledDeviceWithRef.UUID, migratedDevice.UUID, iPhoneMdmDevice.EnrollmentID())
|
|
return err
|
|
})
|
|
}
|
|
|
|
// expire all the certs we just created
|
|
expireCerts()
|
|
|
|
// generate a new config here so we can manipulate the certs.
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
|
|
checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string, wantProfile string, wantManagedAppleID string) {
|
|
var renewCmd *mdm.Command
|
|
cmd, err := device.Idle()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, "InstallProfile", cmd.Command.RequestType)
|
|
renewCmd = cmd
|
|
|
|
require.NotNil(t, renewCmd)
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(renewCmd.Raw, &fullCmd))
|
|
|
|
if wantProfile == "" {
|
|
s.verifyEnrollmentProfile(fullCmd.Command.InstallProfile.Payload, enrollRef, wantManagedAppleID)
|
|
} else {
|
|
p7, err := pkcs7.Parse(fullCmd.Command.InstallProfile.Payload)
|
|
require.NoError(t, err)
|
|
rootCA := x509.NewCertPool()
|
|
|
|
assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
|
|
fleet.MDMAssetCACert,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value))
|
|
require.NoError(t, p7.VerifyWithChain(rootCA))
|
|
require.Equal(t, wantProfile, string(p7.Content))
|
|
}
|
|
|
|
// for testing convenience, we'll acknowledge the command right away, but in practice the
|
|
// device completes the enroll steps (SCEP, Autheniticate, TokenUpdate) before it sends
|
|
// the Acknowledge for the enrollment profile command
|
|
cmd, err = device.Acknowledge(renewCmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
}
|
|
|
|
checkRenewCertCommand(manualEnrolledDevice, "", "", "")
|
|
checkRenewCertCommand(automaticEnrolledDevice, "", "", "")
|
|
checkRenewCertCommand(automaticEnrolledDeviceWithRef, "foo", "", "")
|
|
checkRenewCertCommand(iPhoneMdmDevice, "", "", iphoneUser.Email)
|
|
|
|
// migrated device doesn't receive any commands because
|
|
// `FLEET_SILENT_MIGRATION_ENROLLMENT_PROFILE` is not set
|
|
cmd, err = migratedDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
// set the env var, and run the cron
|
|
t.Setenv("FLEET_SILENT_MIGRATION_ENROLLMENT_PROFILE", base64.StdEncoding.EncodeToString([]byte("<foo></foo>")))
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
checkRenewCertCommand(migratedDevice, "", "<foo></foo>", "")
|
|
|
|
// another cron run shouldn't enqueue more commands
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
|
|
cmd, err = manualEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDeviceWithRef.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = migratedDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = iPhoneMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
// devices renew their SCEP cert by re-enrolling.
|
|
require.NoError(t, manualEnrolledDevice.Reenroll())
|
|
require.NoError(t, automaticEnrolledDevice.Reenroll())
|
|
require.NoError(t, automaticEnrolledDeviceWithRef.Reenroll())
|
|
require.NoError(t, migratedDevice.Reenroll())
|
|
require.NoError(t, iPhoneMdmDevice.Reenroll())
|
|
|
|
// no new commands are enqueued right after enrollment
|
|
cmd, err = manualEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = automaticEnrolledDeviceWithRef.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = migratedDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
cmd, err = iPhoneMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd)
|
|
|
|
// run crons again, no new commands are enqueued
|
|
s.runWorker()
|
|
s.awaitTriggerProfileSchedule(t)
|
|
require.Equal(t, 0, ackAllCommands(manualEnrolledDevice, false, false))
|
|
require.Equal(t, 0, ackAllCommands(automaticEnrolledDevice, false, false))
|
|
require.Equal(t, 0, ackAllCommands(automaticEnrolledDeviceWithRef, false, false))
|
|
require.Equal(t, 0, ackAllCommands(migratedDevice, false, false))
|
|
require.Equal(t, 0, ackAllCommands(iPhoneMdmDevice, false, false))
|
|
|
|
// handle the case of a host being deleted, see https://github.com/fleetdm/fleet/issues/19149
|
|
expireCerts()
|
|
req := deleteHostsRequest{
|
|
IDs: []uint{manualHost.ID},
|
|
}
|
|
resp := deleteHostsResponse{}
|
|
s.DoJSON("POST", "/api/latest/fleet/hosts/delete", req, http.StatusOK, &resp)
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
checkRenewCertCommand(automaticEnrolledDevice, "", "", "")
|
|
checkRenewCertCommand(automaticEnrolledDeviceWithRef, "foo", "", "")
|
|
checkRenewCertCommand(iPhoneMdmDevice, "", "", iphoneUser.Email)
|
|
|
|
// migrated device is still marked as migrated
|
|
var stillMigrated bool
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &stillMigrated, `
|
|
SELECT enrolled_from_migration FROM nano_enrollments
|
|
WHERE id = ?
|
|
`, migratedDevice.UUID)
|
|
})
|
|
require.True(t, stillMigrated)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// iDevice enrolled with legacy enroll reference #37880
|
|
|
|
iPadMdmDevice := mdmtest.NewTestMDMClientAppleOTA(s.server.URL, enrollSecrets[0].Secret, "iPad8,1", mdmtest.WithLegacyIDeviceEnrollRef("some-legacy-ref"))
|
|
require.NoError(t, iPadMdmDevice.Enroll())
|
|
|
|
s.runWorker()
|
|
s.awaitTriggerProfileSchedule(t)
|
|
require.Equal(t, expectedProfiles-1, ackAllCommands(iPadMdmDevice, false, false))
|
|
|
|
// enroll reference wasn't captured for iDevices during enrollment
|
|
require.Empty(t, getEnrollRef(iPadMdmDevice.UUID))
|
|
|
|
// enqueue refetch commands and report results
|
|
require.NoError(t, apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger, func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
return nil
|
|
}))
|
|
require.True(t, existsRefetchCmd(iPadMdmDevice.UUID))
|
|
reportResultsRefetchCmds(iPadMdmDevice)
|
|
|
|
// verify that the enroll reference is now stored in the db
|
|
require.Equal(t, "some-legacy-ref", getEnrollRef(iPadMdmDevice.UUID))
|
|
|
|
// expire cert
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
UPDATE nano_cert_auth_associations
|
|
SET cert_not_valid_after = DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
|
|
WHERE id = ?
|
|
`, iPadMdmDevice.UUID)
|
|
return err
|
|
})
|
|
|
|
// confirm that there's no renew command prior to running the cron
|
|
renewCmdUUID := getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.Empty(t, renewCmdUUID.String)
|
|
require.False(t, renewCmdUUID.Valid)
|
|
|
|
// running cron enqueues the renew command
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
|
|
// we now have a renew command uuid
|
|
renewCmdUUID = getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.NotEmpty(t, renewCmdUUID.String)
|
|
require.True(t, renewCmdUUID.Valid)
|
|
wantCmdUUID := renewCmdUUID.String
|
|
|
|
// verify the renew command includes the enroll reference
|
|
cmd, err = iPadMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, "InstallProfile", cmd.Command.RequestType)
|
|
require.Equal(t, wantCmdUUID, cmd.CommandUUID)
|
|
s.verifyEnrollmentProfile(cmd.Raw, "some-legacy-ref", "")
|
|
|
|
// fail the command so we can test new flow that ensures renew commands are cleared
|
|
// whenever we reset enroll ref
|
|
cmd, err = iPadMdmDevice.Err(cmd.CommandUUID, nil)
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd) // error doesn't trigger new command immediately
|
|
|
|
// running cron again doesn't change anything if the renew command failed
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
cmd, err = iPadMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd) // no new command is enqueued
|
|
|
|
// confirm that the renew command uuid is still the same after failure and cron run
|
|
renewCmdUUID = getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.NotEmpty(t, renewCmdUUID.String)
|
|
require.True(t, renewCmdUUID.Valid)
|
|
require.Equal(t, wantCmdUUID, renewCmdUUID.String)
|
|
|
|
// now clear the enroll_ref to simulate device that failed prior to #37880
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
UPDATE host_mdm
|
|
SET fleet_enroll_ref = ''
|
|
WHERE host_id = (SELECT id FROM hosts WHERE uuid = ?)
|
|
`, iPadMdmDevice.UUID)
|
|
return err
|
|
})
|
|
require.Empty(t, getEnrollRef(iPadMdmDevice.UUID))
|
|
|
|
// running cron again doesn't change anything until refetch is done
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
cmd, err = iPadMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd) // no new command is enqueued
|
|
|
|
// confirm that the renew command uuid is still the same
|
|
renewCmdUUID = getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.NotEmpty(t, renewCmdUUID.String)
|
|
require.True(t, renewCmdUUID.Valid)
|
|
require.Equal(t, wantCmdUUID, renewCmdUUID.String)
|
|
|
|
// backdate host.detail_updated_at so we can do another refetch
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
UPDATE hosts
|
|
SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR)
|
|
WHERE uuid = ?
|
|
`, iPadMdmDevice.UUID)
|
|
return err
|
|
})
|
|
|
|
// enqueue refetch commands and report results
|
|
require.NoError(t, apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger, func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
|
return nil
|
|
}))
|
|
require.True(t, existsRefetchCmd(iPadMdmDevice.UUID))
|
|
reportResultsRefetchCmds(iPadMdmDevice)
|
|
|
|
// refetch triggers new enroll reference to be set and renew commands to be cleared/deactivated
|
|
require.Equal(t, "some-legacy-ref", getEnrollRef(iPadMdmDevice.UUID))
|
|
|
|
// renew command cleared
|
|
renewCmdUUID = getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.Empty(t, renewCmdUUID.String)
|
|
require.False(t, renewCmdUUID.Valid)
|
|
|
|
// nano_enrollment_queue is deactivated
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var active bool
|
|
err := sqlx.GetContext(ctx, q, &active, `
|
|
SELECT active FROM nano_enrollment_queue WHERE id = ? AND command_uuid = ?
|
|
`, iPadMdmDevice.UUID, wantCmdUUID)
|
|
require.NoError(t, err)
|
|
require.False(t, active)
|
|
return nil
|
|
})
|
|
|
|
// now run renewal cron to issue new command
|
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
|
require.NoError(t, err)
|
|
|
|
renewCmdUUID = getRenewCmdUUID(iPadMdmDevice.UUID)
|
|
require.NotEmpty(t, renewCmdUUID.String)
|
|
require.True(t, renewCmdUUID.Valid)
|
|
require.NotEqual(t, wantCmdUUID, renewCmdUUID.String)
|
|
wantCmdUUID = renewCmdUUID.String // updated with new command UUID
|
|
|
|
cmd, err = iPadMdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cmd)
|
|
require.Equal(t, "InstallProfile", cmd.Command.RequestType)
|
|
require.Equal(t, wantCmdUUID, cmd.CommandUUID)
|
|
s.verifyEnrollmentProfile(cmd.Raw, "some-legacy-ref", "")
|
|
|
|
cmd, err = iPadMdmDevice.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, cmd) // no further commands
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestRefetchAfterReenrollIOSNoDelete() {
|
|
t := s.T()
|
|
|
|
triggerRefetchCron := func(hostID uint) {
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(), `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, hostID)
|
|
return err
|
|
})
|
|
trigger := triggerRequest{
|
|
Name: string(fleet.CronAppleMDMIPhoneIPadRefetcher),
|
|
}
|
|
s.Do("POST", "/api/latest/fleet/trigger", trigger, http.StatusOK)
|
|
}
|
|
|
|
awaitRefetchCommands := func(hostID uint, expectCmds int) {
|
|
// Wait until MDM commands are set up
|
|
done := make(chan struct{})
|
|
go func() {
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
commands, err := s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
if len(commands) >= expectCmds {
|
|
done <- struct{}{}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Error("Timeout: MDM commands not queued up")
|
|
}
|
|
}
|
|
|
|
acknowledgeRefetchCommands := func(mdmDevice *mdmtest.TestAppleMDMClient, expectCmds int) {
|
|
cmd, err := mdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
switch cmd.Command.RequestType {
|
|
case "InstalledApplicationList":
|
|
cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{})
|
|
require.NoError(t, err)
|
|
case "CertificateList":
|
|
cmd, err = mdmDevice.AcknowledgeCertificateList(mdmDevice.UUID, cmd.CommandUUID, []*x509.Certificate{})
|
|
require.NoError(t, err)
|
|
case "DeviceInformation":
|
|
cmd, err = mdmDevice.AcknowledgeDeviceInformation(mdmDevice.UUID, cmd.CommandUUID, "Test Name", "iPhone 16", "America/Los_Angeles")
|
|
require.NoError(t, err)
|
|
default:
|
|
require.Fail(t, "unexpected command", cmd.Command.RequestType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// // we're going to modify this mock, make sure we restore its default
|
|
// originalPushMock := s.pushProvider.PushFunc
|
|
// defer func() { s.pushProvider.PushFunc = originalPushMock }()
|
|
|
|
// // FIXME: Figure out the best way to test pushes in the test suite. Can we make this more
|
|
// // user-friendly and reusable?
|
|
// var recordedPushes []*mdm.Push
|
|
// var mu sync.Mutex
|
|
// s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
|
|
// mu.Lock()
|
|
// defer mu.Unlock()
|
|
// recordedPushes = pushes
|
|
// return mockSuccessfulPush(ctx, pushes)
|
|
// }
|
|
|
|
// create a global enroll secret
|
|
globalSecret := "global_secret"
|
|
var applyResp applyEnrollSecretSpecResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
|
Spec: &fleet.EnrollSecretSpec{
|
|
Secrets: []*fleet.EnrollSecret{{Secret: globalSecret}},
|
|
},
|
|
}, http.StatusOK, &applyResp)
|
|
|
|
hwModel := "iPad13,16"
|
|
mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
|
|
s.server.URL,
|
|
"global_secret",
|
|
hwModel,
|
|
)
|
|
require.NoError(t, mdmDevice.Enroll())
|
|
s.runWorker()
|
|
checkInstallFleetdCommandSent(t, mdmDevice, false)
|
|
|
|
// mu.Lock()
|
|
// require.Len(t, recordedPushes, 1)
|
|
// mu.Unlock()
|
|
|
|
hostByIdentifierResp := getHostResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
|
|
require.Equal(t, hwModel, hostByIdentifierResp.Host.HardwareModel)
|
|
require.Equal(t, "ipados", hostByIdentifierResp.Host.Platform)
|
|
require.False(t, hostByIdentifierResp.Host.RefetchRequested)
|
|
hostID := hostByIdentifierResp.Host.ID
|
|
|
|
triggerRefetchCron(hostID)
|
|
awaitRefetchCommands(hostID, 3) // expect three commands: refetch UUID, apps, certs
|
|
|
|
hostByIdentifierResp = getHostResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
|
|
require.False(t, hostByIdentifierResp.Host.RefetchRequested) // refetch cron doesn't set the refetch_requested flag
|
|
|
|
// mu.Lock()
|
|
// require.Len(t, recordedPushes, 4)
|
|
// mu.Unlock()
|
|
|
|
acknowledgeRefetchCommands(mdmDevice, 3)
|
|
commands, err := s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, commands, 0) // after acknowledging the commands, there should be no more commands
|
|
|
|
triggerRefetchCron(hostID)
|
|
awaitRefetchCommands(hostID, 3) // expect three new commands from the cron
|
|
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, commands, 3) // three new refetch commands from the cron
|
|
cmdTypes := make([]string, 0, len(commands))
|
|
for _, cmd := range commands {
|
|
cmdTypes = append(cmdTypes, cmd.CommandType)
|
|
}
|
|
require.ElementsMatch(t, []string{fleet.RefetchDeviceCommandUUIDPrefix, fleet.RefetchAppsCommandUUIDPrefix, fleet.RefetchCertsCommandUUIDPrefix}, cmdTypes)
|
|
|
|
// re-enroll the device
|
|
require.NoError(t, mdmDevice.Enroll())
|
|
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, commands, 0) // re-enrollment clears existing commands
|
|
|
|
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", hostID), nil, http.StatusOK)
|
|
hostByIdentifierResp = getHostResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
|
|
require.True(t, hostByIdentifierResp.Host.RefetchRequested)
|
|
|
|
awaitRefetchCommands(hostID, 3) // expect three commands: refetch UUID, apps, certs
|
|
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, commands, 3)
|
|
|
|
// re-enroll the device
|
|
require.NoError(t, mdmDevice.Enroll())
|
|
commands, err = s.ds.GetHostMDMCommands(context.Background(), hostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, commands, 0)
|
|
|
|
hostByIdentifierResp = getHostResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
|
|
require.False(t, hostByIdentifierResp.Host.RefetchRequested) // re-enrollment also clears the refetch_requested flag
|
|
}
|
|
|
|
// TestMDMLockHostUnenrolled tests that we cannot lock a macOS host that is not enrolled in MDM.
|
|
// See https://github.com/fleetdm/fleet/issues/30192
|
|
func (s *integrationMDMTestSuite) TestMDMLockHostUnenrolled() {
|
|
t := s.T()
|
|
|
|
// create a global enroll secret
|
|
globalSecret := "global_secret"
|
|
var applyResp applyEnrollSecretSpecResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
|
Spec: &fleet.EnrollSecretSpec{
|
|
Secrets: []*fleet.EnrollSecret{{Secret: globalSecret}},
|
|
},
|
|
}, http.StatusOK, &applyResp)
|
|
|
|
hwModel := "MacBookPro14,3"
|
|
mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
|
|
s.server.URL,
|
|
"global_secret",
|
|
hwModel,
|
|
)
|
|
require.NoError(t, mdmDevice.Enroll())
|
|
|
|
hostByIdentifierResp := getHostResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
|
|
require.Equal(t, hwModel, hostByIdentifierResp.Host.HardwareModel)
|
|
hostID := hostByIdentifierResp.Host.ID
|
|
|
|
// mark the host as unenrolled in MDM
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(), `
|
|
UPDATE nano_enrollments
|
|
SET enabled = 0
|
|
WHERE id = ?
|
|
`, mdmDevice.UUID)
|
|
return err
|
|
})
|
|
|
|
// try to lock the host, it should fail because the host is not enrolled
|
|
res := s.Do(
|
|
"POST",
|
|
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", hostID),
|
|
nil,
|
|
http.StatusUnprocessableEntity,
|
|
)
|
|
defer res.Body.Close()
|
|
|
|
e := extractServerErrorText(res.Body)
|
|
require.Contains(t, e, "Can't lock the host because it doesn't have MDM turned on")
|
|
}
|
|
|
|
// Test case for https://github.com/fleetdm/fleet/issues/33074
|
|
func (s *integrationMDMTestSuite) TestFileVaultProfileUpdatedOnMDMToggle() {
|
|
t := s.T()
|
|
|
|
// Setup Apple MDM
|
|
s.appleCoreCertsSetup()
|
|
|
|
// Turn on disk encryption for no team
|
|
s.Do("POST", "/api/latest/fleet/disk_encryption", json.RawMessage(`{
|
|
"enable_disk_encryption": true
|
|
}`), http.StatusNoContent)
|
|
|
|
// Check that FileVault profile exists in the database
|
|
var initialProfileID uint
|
|
var initialTimestamp time.Time
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return q.QueryRowxContext(context.Background(),
|
|
`SELECT profile_id, uploaded_at FROM mdm_apple_configuration_profiles
|
|
WHERE identifier = ? AND team_id = 0`,
|
|
mobileconfig.FleetFileVaultPayloadIdentifier).Scan(&initialProfileID, &initialTimestamp)
|
|
})
|
|
require.NotZero(t, initialProfileID, "FileVault profile should exist in database after enabling disk encryption")
|
|
|
|
// Wait a moment to ensure timestamp difference will be detectable
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Turn off Apple MDM
|
|
s.Do("DELETE", "/api/latest/fleet/mdm/apple/apns_certificate", nil, http.StatusOK)
|
|
|
|
// Turn back on Apple MDM
|
|
s.appleCoreCertsSetup()
|
|
|
|
// Check that FileVault profile still exists and has been updated
|
|
var updatedProfileID uint
|
|
var updatedTimestamp time.Time
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return q.QueryRowxContext(context.Background(),
|
|
`SELECT profile_id, uploaded_at FROM mdm_apple_configuration_profiles
|
|
WHERE identifier = ? AND team_id = 0`,
|
|
mobileconfig.FleetFileVaultPayloadIdentifier).Scan(&updatedProfileID, &updatedTimestamp)
|
|
})
|
|
require.NotZero(t, updatedProfileID, "FileVault profile should exist in database after re-enabling MDM")
|
|
|
|
// Verify the profile has been updated (newer timestamp or different profile ID)
|
|
profileWasUpdated := updatedTimestamp.After(initialTimestamp) || updatedProfileID != initialProfileID
|
|
require.True(t, profileWasUpdated,
|
|
"FileVault profile should have been updated when MDM was re-enabled. Initial ID: %d (time: %v), Updated ID: %d (time: %v)",
|
|
initialProfileID, initialTimestamp, updatedProfileID, updatedTimestamp)
|
|
|
|
// Disable MDM and remove filevault profile, then re-enable
|
|
s.Do("DELETE", "/api/latest/fleet/mdm/apple/apns_certificate", nil, http.StatusOK)
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(),
|
|
`DELETE FROM mdm_apple_configuration_profiles
|
|
WHERE identifier = ? AND team_id = 0`,
|
|
mobileconfig.FleetFileVaultPayloadIdentifier)
|
|
return err
|
|
})
|
|
|
|
// Turn back on MDM, and see it succesfully creates the profile without fail
|
|
s.appleCoreCertsSetup()
|
|
|
|
var finalProfileID uint
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
return q.QueryRowxContext(context.Background(),
|
|
`SELECT profile_id FROM mdm_apple_configuration_profiles
|
|
WHERE identifier = ? AND team_id = 0`,
|
|
mobileconfig.FleetFileVaultPayloadIdentifier).Scan(&finalProfileID)
|
|
})
|
|
require.NotZero(t, finalProfileID, "FileVault profile should exist in database after re-enabling MDM when it was previously deleted")
|
|
}
|