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 #38889 PLEASE READ BELOW before looking at file changes Before converting individual files/packages to slog, we generally need to make these 2 changes to make the conversion easier: - Replace uses of `kitlog.With` since they are not fully compatible with our kitlog adapter - Directly use the kitlog adapter logger type instead of the kitlog interface, which will let us have direct access to the underlying slog logger: `*logging.Logger` Note: that I did not replace absolutely all uses of `kitlog.Logger`, but I did remove all uses of `kitlog.With` except for these due to complexity: - server/logging/filesystem.go and the other log writers (webhook, firehose, kinesis, lambda, pubsub, nats) - server/datastore/mysql/nanomdm_storage.go (adapter pattern) - server/vulnerabilities/nvd/* (cascades to CLI tools) - server/service/osquery_utils/queries.go (callback type signatures cascade broadly) - cmd/maintained-apps/ (standalone, so can be transitioned later all at once) Most of the changes in this PR follow these patterns: - `kitlog.Logger` type → `*logging.Logger` - `kitlog.With(logger, ...)` → `logger.With(...)` - `kitlog.NewNopLogger() → logging.NewNopLogger()`, including similar variations such as `logging.NewLogfmtLogger(w)` and `logging.NewJSONLogger(w)` - removed many now-unused kitlog imports Unique changes that the PR review should focus on: - server/platform/logging/kitlog_adapter.go: Core adapter changes - server/platform/logging/logging.go: New convenience functions - server/service/integration_logger_test.go: Test changes for slog # 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`. - Was added in previous PR ## 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 * **Refactor** * Migrated the codebase to a unified internal structured logging system for more consistent, reliable logs and observability. * No user-facing functionality changed; runtime behavior and APIs remain compatible. * **Tests** * Updated tests to use the new logging helpers to ensure consistent test logging and validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1532 lines
52 KiB
Go
1532 lines
52 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"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/platform/logging"
|
|
"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")
|
|
}
|
|
|
|
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.TestWindowsMDMClientWithSigningKey(s.jwtSigningKey, defaultFakeJWTKeyID))
|
|
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 {
|
|
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 := logging.NewJSONLogger(os.Stdout)
|
|
|
|
// 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.Enroll())
|
|
require.NoError(t, automaticEnrolledDevice.Enroll())
|
|
require.NoError(t, automaticEnrolledDeviceWithRef.Enroll())
|
|
require.NoError(t, migratedDevice.Enroll())
|
|
require.NoError(t, iPhoneMdmDevice.Enroll())
|
|
|
|
// 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")
|
|
}
|