A sample of this query's results is included
diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
index c27ab54685..43fed46962 100644
--- a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
+++ b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
@@ -11,6 +11,7 @@ import {
generateCSVQueryResults,
} from "utilities/generate_csv";
import { getTableColumnsFromSql } from "utilities/helpers";
+import { SUPPORT_LINK } from "utilities/constants";
import { ICampaign, ICampaignError } from "interfaces/campaign";
import { ITarget } from "interfaces/target";
@@ -263,13 +264,7 @@ const QueryResults = ({
{isQueryClipped && (
Results clipped. A sample of this query's results and
diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts
index 5d5c41029d..6a13a49aec 100644
--- a/frontend/services/mock_service/mocks/config.ts
+++ b/frontend/services/mock_service/mocks/config.ts
@@ -23,6 +23,7 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = {
// expensive data operations
"targets?query={*}": RESPONSES.hosts,
// "SchedulableQueries" to be used in developing frontend for #7765
+ "hosts/12345": RESPONSES.hostDetailsiOS,
queries: RESPONSES.globalQueries,
"queries/1": RESPONSES.globalQuery1,
"queries/2": RESPONSES.globalQuery2,
diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts
index 709abc3f8b..265b6dd494 100644
--- a/frontend/services/mock_service/mocks/responses.ts
+++ b/frontend/services/mock_service/mocks/responses.ts
@@ -4,6 +4,7 @@
* Also please check the README for how to use the mock service :)
*/
+import { createMockIosHostResponse } from "__mocks__/hostMock";
import { createMockPoliciesResponse } from "__mocks__/policyMock";
const count = {
@@ -10593,7 +10594,7 @@ const globalQuery6 = { query: globalQueries.queries[6] };
const teamQuery1 = { query: teamQueries.queries[0] };
const teamQuery2 = { query: teamQueries.queries[1] };
const teamPolicy1 = createMockPoliciesResponse();
-
+const hostDetailsiOS = createMockIosHostResponse;
const aiAutofillPolicy = {
description:
"The firewall is not enabled, exposing the laptop to potential security threats such as unauthorized access, data breaches, and malware attacks.",
@@ -10618,4 +10619,5 @@ export default {
teamQuery2,
aiAutofillPolicy,
teamPolicy1,
+ hostDetailsiOS,
};
diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx
index 74a06ee3cd..3c5235c61c 100644
--- a/frontend/utilities/constants.tsx
+++ b/frontend/utilities/constants.tsx
@@ -387,6 +387,7 @@ export const HOST_ABOUT_DATA = [
"batteries",
"detail_updated_at",
"last_restarted_at",
+ "platform",
];
export const HOST_OSQUERY_DATA = [
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index be5ecd2148..621c763999 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -707,7 +707,7 @@ WHERE
func (ds *Datastore) MDMAppleUpsertHost(ctx context.Context, mdmHost *fleet.Host) error {
appCfg, err := ds.AppConfig(ctx)
if err != nil {
- return ctxerr.Wrap(ctx, err, "ingest mdm apple host get app config")
+ return ctxerr.Wrap(ctx, err, "mdm apple upsert host get app config")
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
return ingestMDMAppleDeviceFromCheckinDB(ctx, tx, mdmHost, ds.logger, appCfg)
@@ -743,6 +743,20 @@ func ingestMDMAppleDeviceFromCheckinDB(
}
}
+func mdmHostEnrollFields(mdmHost *fleet.Host) (refetchRequested bool, lastEnrolledAt time.Time) {
+ supportsOsquery := mdmHost.SupportsOsquery()
+ // 2000-01-01 00:00:00 is what Fleet considers the zero/"Never" time.
+ lastEnrolledAt, err := time.Parse("2006-01-02 15:04:05", "2000-01-01 00:00:00")
+ if err != nil {
+ panic(err)
+ }
+ if !supportsOsquery {
+ // Given the device does not have osquery, we set the last_enrolled_at as the MDM enroll time.
+ lastEnrolledAt = time.Now()
+ }
+ return supportsOsquery, lastEnrolledAt
+}
+
func updateMDMAppleHostDB(
ctx context.Context,
tx sqlx.ExtContext,
@@ -750,6 +764,8 @@ func updateMDMAppleHostDB(
mdmHost *fleet.Host,
appCfg *fleet.AppConfig,
) error {
+ refetchRequested, lastEnrolledAt := mdmHostEnrollFields(mdmHost)
+
updateStmt := `
UPDATE hosts SET
hardware_serial = ?,
@@ -757,6 +773,7 @@ func updateMDMAppleHostDB(
hardware_model = ?,
platform = ?,
refetch_requested = ?,
+ last_enrolled_at = ?,
osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?)
WHERE id = ?`
@@ -766,8 +783,9 @@ func updateMDMAppleHostDB(
mdmHost.HardwareSerial,
mdmHost.UUID,
mdmHost.HardwareModel,
- "darwin",
- 1,
+ mdmHost.Platform,
+ refetchRequested,
+ lastEnrolledAt,
// Set osquery_host_id to the device UUID only if it is not already set.
mdmHost.UUID,
hostID,
@@ -794,6 +812,7 @@ func insertMDMAppleHostDB(
logger log.Logger,
appCfg *fleet.AppConfig,
) error {
+ refetchRequested, lastEnrolledAt := mdmHostEnrollFields(mdmHost)
insertStmt := `
INSERT INTO hosts (
hardware_serial,
@@ -812,11 +831,11 @@ func insertMDMAppleHostDB(
mdmHost.HardwareSerial,
mdmHost.UUID,
mdmHost.HardwareModel,
- "darwin",
- "2000-01-01 00:00:00",
+ mdmHost.Platform,
+ lastEnrolledAt,
"2000-01-01 00:00:00",
mdmHost.UUID,
- 1,
+ refetchRequested,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "insert mdm apple host")
@@ -896,18 +915,18 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic
SELECT
us.hardware_serial,
COALESCE(GROUP_CONCAT(DISTINCT us.hardware_model), ''),
- 'darwin' AS platform,
+ us.platform,
'2000-01-01 00:00:00' AS last_enrolled_at,
'2000-01-01 00:00:00' AS detail_updated_at,
NULL AS osquery_host_id,
- 1 AS refetch_requested,
+ IF(us.platform = 'ios' OR us.platform = 'ipados', 0, 1) AS refetch_requested,
? AS team_id
FROM (%s) us
LEFT JOIN hosts h ON us.hardware_serial = h.hardware_serial
WHERE
h.id IS NULL
GROUP BY
- us.hardware_serial)`,
+ us.hardware_serial, us.platform)`,
us,
)
@@ -933,6 +952,7 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic
err = sqlx.SelectContext(ctx, tx, &hostsWithMDMInfo, fmt.Sprintf(`
SELECT
h.id,
+ h.platform,
h.hardware_model,
h.hardware_serial,
COALESCE(hmdm.enrolled, 0) as enrolled
@@ -1096,25 +1116,42 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext
// now because it may still be some time before osquery is running on these
// devices. Because these are Apple devices, we're adding them to the "All
// Hosts" and "macOS" labels.
- labelIDs := []uint{}
- err := sqlx.SelectContext(ctx, tx, &labelIDs, `SELECT id FROM labels WHERE label_type = 1 AND (name = 'All Hosts' OR name = 'macOS')`)
+ labels := []struct {
+ ID uint `db:"id"`
+ Name string `db:"name"`
+ }{}
+ err := sqlx.SelectContext(ctx, tx, &labels, `SELECT id, name FROM labels WHERE label_type = 1 AND (name = 'All Hosts' OR name = 'macOS')`)
switch {
case err != nil:
return ctxerr.Wrap(ctx, err, "get builtin labels")
- case len(labelIDs) != 2:
+ case len(labels) != 2:
// Builtin labels can get deleted so it is important that we check that
// they still exist before we continue.
- level.Error(logger).Log("err", fmt.Sprintf("expected 2 builtin labels but got %d", len(labelIDs)))
+ level.Error(logger).Log("err", fmt.Sprintf("expected 2 builtin labels but got %d", len(labels)))
return nil
default:
// continue
}
+ // Put "All Hosts" label first (we don't want to make assumptions around ids of builtin labels).
+ labelIDs := make([]uint, 0, 2)
+ if labels[0].Name == "All Hosts" {
+ labelIDs = append(labelIDs, labels[0].ID, labels[1].ID)
+ } else {
+ labelIDs = append(labelIDs, labels[1].ID, labels[0].ID)
+ }
+
parts := []string{}
args := []interface{}{}
for _, h := range hosts {
- parts = append(parts, "(?,?),(?,?)")
- args = append(args, h.ID, labelIDs[0], h.ID, labelIDs[1])
+ // iOS/iPadOS devices only get the "All Hosts" label.
+ if h.Platform == "ios" || h.Platform == "ipados" {
+ parts = append(parts, "(?,?)")
+ args = append(args, h.ID, labelIDs[0])
+ } else { // macOS devices get both labels, "All Hosts" and "macOS".
+ parts = append(parts, "(?,?),(?,?)")
+ args = append(args, h.ID, labelIDs[0], h.ID, labelIDs[1])
+ }
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO label_membership (host_id, label_id) VALUES %s
@@ -1131,6 +1168,8 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext
func (ds *Datastore) deleteMDMOSCustomSettingsForHost(ctx context.Context, tx sqlx.ExtContext, uuid, platform string) error {
tableMap := map[string][]string{
"darwin": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"},
+ "ios": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"},
+ "ipados": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"},
"windows": {"host_mdm_windows_profiles"},
}
@@ -1162,8 +1201,8 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error {
return ctxerr.Wrap(ctx, err, "getting host info from UUID")
}
- if host.Platform != "darwin" && host.Platform != "windows" {
- return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform)
+ if !fleet.MDMSupported(host.Platform) {
+ return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform)
}
// NOTE: set installed_from_dep = 0 so DEP host will not be
@@ -1192,6 +1231,11 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error {
// NOTE: intentionally keeping disk encryption keys and bootstrap
// package information.
+ // iPhones and iPads have no osquery thus we don't need to refetch.
+ if host.Platform == "ios" || host.Platform == "ipados" {
+ return nil
+ }
+
// request a refetch to update any eventually consistent stale information.
err = updateHostRefetchRequestedDB(ctx, tx, host.ID, true)
return ctxerr.Wrap(ctx, err, "setting host refetch requested")
@@ -1201,11 +1245,19 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error {
func unionSelectDevices(devices []godep.Device) (stmt string, args []interface{}) {
for i, d := range devices {
if i == 0 {
- stmt = "SELECT ? hardware_serial, ? hardware_model"
+ stmt = "SELECT ? hardware_serial, ? hardware_model, ? platform"
} else {
- stmt += " UNION SELECT ?, ?"
+ stmt += " UNION SELECT ?, ?, ?"
}
- args = append(args, d.SerialNumber, d.Model)
+ // Map Apple's device family to Fleet's hosts.platform field.
+ platform := "darwin"
+ switch d.DeviceFamily {
+ case "iPhone":
+ platform = "ios"
+ case "iPad":
+ platform = "ipados"
+ }
+ args = append(args, d.SerialNumber, d.Model, platform)
}
return stmt, args
@@ -1590,6 +1642,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
SELECT
ds.profile_uuid as profile_uuid,
ds.host_uuid as host_uuid,
+ ds.host_platform as host_platform,
ds.profile_identifier as profile_identifier,
ds.profile_name as profile_name,
ds.checksum as checksum
@@ -1619,6 +1672,9 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute")
}
+ // Exclude macOS only profiles from iPhones/iPads.
+ wantedProfiles = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(wantedProfiles)
+
toRemoveStmt := fmt.Sprintf(`
SELECT
hmap.profile_uuid as profile_uuid,
@@ -1817,6 +1873,7 @@ func generateDesiredStateQuery(entityType string) string {
SELECT
mae.%[1]s_uuid,
h.uuid as host_uuid,
+ h.platform as host_platform,
mae.identifier as %[1]s_identifier,
mae.name as %[1]s_name,
mae.checksum as checksum,
@@ -1829,7 +1886,7 @@ func generateDesiredStateQuery(entityType string) string {
JOIN nano_enrollments ne
ON ne.device_id = h.uuid
WHERE
- h.platform = 'darwin' AND
+ (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND
ne.enabled = 1 AND
ne.type = 'Device' AND
NOT EXISTS (
@@ -1845,6 +1902,7 @@ func generateDesiredStateQuery(entityType string) string {
SELECT
mae.%[1]s_uuid,
h.uuid as host_uuid,
+ h.platform as host_platform,
mae.identifier as %[1]s_identifier,
mae.name as %[1]s_name,
mae.checksum as checksum,
@@ -1861,12 +1919,12 @@ func generateDesiredStateQuery(entityType string) string {
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mel.label_id AND lm.host_id = h.id
WHERE
- h.platform = 'darwin' AND
+ (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND
ne.enabled = 1 AND
ne.type = 'Device' AND
( %[3]s )
GROUP BY
- mae.%[1]s_uuid, h.uuid, mae.identifier, mae.name, mae.checksum
+ mae.%[1]s_uuid, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum
HAVING
count_%[1]s_labels > 0 AND count_host_labels = count_%[1]s_labels
@@ -1978,6 +2036,7 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee
SELECT
ds.profile_uuid,
ds.host_uuid,
+ ds.host_platform,
ds.profile_identifier,
ds.profile_name,
ds.checksum
@@ -2466,7 +2525,8 @@ SELECT
COUNT(id) as count
FROM
hosts h
-GROUP BY status, platform, team_id HAVING platform = 'darwin' AND status IN (?, ?, ?, ?) AND %s`
+WHERE platform = 'darwin' OR platform = 'ios' OR platform = 'ipados'
+GROUP BY status, team_id HAVING status IN (?, ?, ?, ?) AND %s`
args = append(args, fleet.MDMDeliveryFailed, fleet.MDMDeliveryPending, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified)
@@ -3415,8 +3475,8 @@ func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string) er
return ctxerr.Wrap(ctx, err, "getting host info from UUID")
}
- if host.Platform != "darwin" && host.Platform != "windows" {
- return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform)
+ if !fleet.MDMSupported(host.Platform) {
+ return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform)
}
// Deleting profiles from this table will cause all profiles to
@@ -4116,3 +4176,20 @@ VALUES
return nil
}
+
+// ListIOSAndIPadOSToRefetch returns the UUIDs of iPhones/iPads that should be refetched
+// (their details haven't been updated in the given `interval`).
+func (ds *Datastore) ListIOSAndIPadOSToRefetch(ctx context.Context, interval time.Duration) (uuids []string, err error) {
+ var deviceUUIDs []string
+ hostsStmt := fmt.Sprintf(`
+SELECT h.uuid FROM hosts h
+JOIN host_mdm hmdm ON hmdm.host_id = h.id
+WHERE (h.platform = 'ios' OR h.platform = 'ipados')
+AND hmdm.enrolled
+AND TIMESTAMPDIFF(SECOND, h.detail_updated_at, NOW()) > ?;`)
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &deviceUUIDs, hostsStmt, interval.Seconds()); err != nil {
+ return nil, err
+ }
+
+ return deviceUUIDs, nil
+}
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index 43ec3a7812..d1547644f7 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -1,6 +1,7 @@
package mysql
import (
+ "bytes"
"context"
"crypto/md5" // nolint:gosec // used only to hash for efficient comparisons
"crypto/sha256"
@@ -8,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "sort"
"strings"
"testing"
"time"
@@ -74,6 +76,10 @@ func TestMDMApple(t *testing.T) {
{"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs},
{"SetOrUpdateMDMAppleDeclaration", testSetOrUpdateMDMAppleDDMDeclaration},
{"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates},
+ {"ListIOSAndIPadOSToRefetch", testListIOSAndIPadOSToRefetch},
+ {"MDMAppleUpsertHostIOSiPadOS", testMDMAppleUpsertHostIOSIPadOS},
+ {"IngestMDMAppleDevicesFromDEPSyncIOSIPadOS", testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS},
+ {"MDMAppleProfilesOnIOSIPadOS", testMDMAppleProfilesOnIOSIPadOS},
}
for _, c := range cases {
@@ -787,6 +793,7 @@ func testIngestMDMNonDarwinHostAlreadyExistsInFleet(t *testing.T, ds *Datastore)
err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
UUID: testUUID,
HardwareSerial: testSerial,
+ Platform: "darwin",
})
require.NoError(t, err)
@@ -904,6 +911,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
UUID: testUUID,
HardwareSerial: testSerial,
+ Platform: "darwin",
})
require.NoError(t, err)
@@ -1393,9 +1401,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
require.NoError(t, err)
matchProfiles([]*fleet.MDMAppleProfilePayload{
- {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"},
+ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
}, profiles)
// add another host, it belongs to a team
@@ -1416,9 +1424,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
require.NoError(t, err)
matchProfiles([]*fleet.MDMAppleProfilePayload{
- {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"},
+ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
}, profiles)
// assign profiles to team 1
@@ -1440,11 +1448,11 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
require.NoError(t, err)
matchProfiles([]*fleet.MDMAppleProfilePayload{
- {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2"},
- {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2"},
+ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
+ {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
}, profiles)
// add another global host
@@ -1463,14 +1471,14 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
require.NoError(t, err)
matchProfiles([]*fleet.MDMAppleProfilePayload{
- {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2"},
- {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2"},
- {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3"},
- {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3"},
- {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3"},
+ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
+ {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
+ {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"},
}, profiles)
// cron runs and updates the status
@@ -1597,8 +1605,8 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) {
profiles, err = ds.ListMDMAppleProfilesToInstall(ctx)
require.NoError(t, err)
matchProfiles([]*fleet.MDMAppleProfilePayload{
- {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1"},
- {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1"},
+ {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
+ {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"},
}, profiles)
// profiles to be removed includes host1's old profiles
@@ -5497,3 +5505,270 @@ func createRawAppleCmd(reqType, cmdUUID string) string {
`, reqType, cmdUUID)
}
+
+func testListIOSAndIPadOSToRefetch(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ refetchInterval := 1 * time.Hour
+ hostCount := 0
+ newHost := func(platform string) *fleet.Host {
+ h, err := ds.NewHost(ctx, &fleet.Host{
+ Hostname: fmt.Sprintf("foobar%d", hostCount),
+ OsqueryHostID: ptr.String(fmt.Sprintf("foobar-%d", hostCount)),
+ NodeKey: ptr.String(fmt.Sprintf("foobar-%d", hostCount)),
+ UUID: fmt.Sprintf("foobar-%d", hostCount),
+ Platform: platform,
+ HardwareSerial: fmt.Sprintf("foobar-%d", hostCount),
+ })
+ require.NoError(t, err)
+ hostCount++
+ return h
+ }
+
+ // Test with no hosts.
+ uuids, err := ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Empty(t, uuids)
+
+ // Create a placeholder macOS host.
+ _ = newHost("darwin")
+
+ // Mock results incoming from depsync.Syncer
+ depDevices := []godep.Device{
+ {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"},
+ {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"},
+ }
+ n, _, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
+ require.NoError(t, err)
+ require.Equal(t, int64(2), n)
+
+ // Hosts are not enrolled yet (e.g. DEP enrolled)
+ uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Empty(t, uuids)
+
+ // Now simulate the initial MDM checkin of the devices.
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: "iOS0_UUID",
+ HardwareSerial: "iOS0_SERIAL",
+ HardwareModel: "iPhone14,6",
+ Platform: "ios",
+ OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"),
+ })
+ require.NoError(t, err)
+ iOS0, err := ds.HostByIdentifier(ctx, "iOS0_SERIAL")
+ require.NoError(t, err)
+ nanoEnroll(t, ds, iOS0, false)
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: "iPadOS0_UUID",
+ HardwareSerial: "iPadOS0_SERIAL",
+ HardwareModel: "iPad13,18",
+ Platform: "ipados",
+ OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"),
+ })
+ require.NoError(t, err)
+ iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_SERIAL")
+ require.NoError(t, err)
+ nanoEnroll(t, ds, iPadOS0, false)
+
+ // Test with hosts but empty state in nanomdm command tables.
+ uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Len(t, uuids, 2)
+ sort.Slice(uuids, func(i, j int) bool {
+ return uuids[i] < uuids[j]
+ })
+ require.Equal(t, uuids, []string{"iOS0_UUID", "iPadOS0_UUID"})
+
+ // Set iOS detail_updated_at as 30 minutes in the past.
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iOS0.ID)
+ return err
+ })
+
+ // iOS device should not be returned because it was refetched recently
+ uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Len(t, uuids, 1)
+ require.Equal(t, uuids[0], "iPadOS0_UUID")
+
+ // Set iPadOS detail_updated_at as 30 minutes in the past.
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iPadOS0.ID)
+ return err
+ })
+
+ // Both devices are up-to-date thus none should be returned.
+ uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Empty(t, uuids)
+
+ // Set iOS detail_updated_at as 2 hours in the past.
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, iOS0.ID)
+ return err
+ })
+
+ // iOS device be returned because it is out of date.
+ uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval)
+ require.NoError(t, err)
+ require.Len(t, uuids, 1)
+ require.Equal(t, uuids[0], "iOS0_UUID")
+}
+
+func testMDMAppleUpsertHostIOSIPadOS(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ createBuiltinLabels(t, ds)
+
+ for i, platform := range []string{"ios", "ipados"} {
+ // Upsert first to test insertMDMAppleHostDB.
+ err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: fmt.Sprintf("test-uuid-%d", i),
+ HardwareSerial: fmt.Sprintf("test-serial-%d", i),
+ HardwareModel: "test-hw-model",
+ Platform: platform,
+ })
+ require.NoError(t, err)
+ h, err := ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i))
+ require.NoError(t, err)
+ require.Equal(t, false, h.RefetchRequested)
+ require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never".
+ require.Equal(t, "test-hw-model", h.HardwareModel)
+
+ labels, err := ds.ListLabelsForHost(ctx, h.ID)
+ require.NoError(t, err)
+ require.Len(t, labels, 1)
+ require.Equal(t, "All Hosts", labels[0].Name)
+
+ // Insert again to test updateMDMAppleHostDB.
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: fmt.Sprintf("test-uuid-%d", i),
+ HardwareSerial: fmt.Sprintf("test-serial-%d", i),
+ HardwareModel: "test-hw-model-2",
+ Platform: platform,
+ })
+ require.NoError(t, err)
+ h, err = ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i))
+ require.NoError(t, err)
+ require.Equal(t, false, h.RefetchRequested)
+ require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never".
+ require.Equal(t, "test-hw-model-2", h.HardwareModel)
+
+ labels, err = ds.ListLabelsForHost(ctx, h.ID)
+ require.NoError(t, err)
+ require.Len(t, labels, 1)
+ require.Equal(t, "All Hosts", labels[0].Name)
+ }
+
+ err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: "test-uuid-2",
+ HardwareSerial: "test-serial-2",
+ HardwareModel: "test-hw-model",
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+ h, err := ds.HostByIdentifier(ctx, "test-uuid-2")
+ require.NoError(t, err)
+ require.Equal(t, true, h.RefetchRequested)
+ require.Less(t, 1*time.Hour, time.Since(h.LastEnrolledAt)) // check it's in the date in the 2000 we use as "Never".
+ labels, err := ds.ListLabelsForHost(ctx, h.ID)
+ require.NoError(t, err)
+ require.Len(t, labels, 2)
+ require.Equal(t, "All Hosts", labels[0].Name)
+ require.Equal(t, "macOS", labels[1].Name)
+}
+
+func testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ // Mock results incoming from depsync.Syncer
+ depDevices := []godep.Device{
+ {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"},
+ {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"},
+ }
+
+ n, _, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices)
+ require.NoError(t, err)
+ require.Equal(t, int64(2), n)
+
+ hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{
+ User: &fleet.User{
+ GlobalRole: ptr.String(fleet.RoleAdmin),
+ },
+ }, fleet.HostListOptions{})
+ require.NoError(t, err)
+ require.Len(t, hosts, 2)
+ require.Equal(t, "ios", hosts[0].Platform)
+ require.Equal(t, false, hosts[0].RefetchRequested)
+ require.Equal(t, "ipados", hosts[1].Platform)
+ require.Equal(t, false, hosts[1].RefetchRequested)
+}
+
+func testMDMAppleProfilesOnIOSIPadOS(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ // Add the Fleetd configuration and profile that are only for macOS.
+ params := mobileconfig.FleetdProfileOptions{
+ EnrollSecret: t.Name(),
+ ServerURL: "https://example.com",
+ PayloadType: mobileconfig.FleetdConfigPayloadIdentifier,
+ PayloadName: fleetmdm.FleetdConfigProfileName,
+ }
+ var contents bytes.Buffer
+ err := mobileconfig.FleetdProfileTemplate.Execute(&contents, params)
+ require.NoError(t, err)
+ fleetdConfigProfile, err := fleet.NewMDMAppleConfigProfile(contents.Bytes(), nil)
+ require.NoError(t, err)
+ _, err = ds.NewMDMAppleConfigProfile(ctx, *fleetdConfigProfile)
+ require.NoError(t, err)
+
+ // For the FileVault profile we re-use the FleetdProfileTemplate
+ // (because fileVaultProfileTemplate is not exported)
+ var contents2 bytes.Buffer
+ params.PayloadName = fleetmdm.FleetFileVaultProfileName
+ params.PayloadType = mobileconfig.FleetFileVaultPayloadIdentifier
+ err = mobileconfig.FleetdProfileTemplate.Execute(&contents2, params)
+ require.NoError(t, err)
+ fileVaultProfile, err := fleet.NewMDMAppleConfigProfile(contents2.Bytes(), nil)
+ require.NoError(t, err)
+ _, err = ds.NewMDMAppleConfigProfile(ctx, *fileVaultProfile)
+ require.NoError(t, err)
+
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: "iOS0_UUID",
+ HardwareSerial: "iOS0_SERIAL",
+ HardwareModel: "iPhone14,6",
+ Platform: "ios",
+ OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"),
+ })
+ require.NoError(t, err)
+ iOS0, err := ds.HostByIdentifier(ctx, "iOS0_UUID")
+ require.NoError(t, err)
+ nanoEnroll(t, ds, iOS0, false)
+ err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{
+ UUID: "iPadOS0_UUID",
+ HardwareSerial: "iPadOS0_SERIAL",
+ HardwareModel: "iPad13,18",
+ Platform: "ipados",
+ OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"),
+ })
+ require.NoError(t, err)
+ iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_UUID")
+ require.NoError(t, err)
+ nanoEnroll(t, ds, iPadOS0, false)
+
+ someProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("a", "a", 0))
+ require.NoError(t, err)
+
+ err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
+ require.NoError(t, err)
+
+ profiles, err := ds.GetHostMDMAppleProfiles(ctx, "iOS0_UUID")
+ require.NoError(t, err)
+ require.Len(t, profiles, 1)
+ require.Equal(t, someProfile.Name, profiles[0].Name)
+ profiles, err = ds.GetHostMDMAppleProfiles(ctx, "iPadOS0_UUID")
+ require.NoError(t, err)
+ require.Len(t, profiles, 1)
+ require.Equal(t, someProfile.Name, profiles[0].Name)
+}
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index b9c64045cd..09248b1324 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -1188,7 +1188,7 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{
}
}
if opt.MDMNameFilter != nil || opt.MDMIDFilter != nil || opt.MDMEnrollmentStatusFilter != "" {
- sql += ` AND NOT COALESCE(hmdm.is_server, false) AND h.platform IN('darwin', 'windows')`
+ sql += ` AND NOT COALESCE(hmdm.is_server, false) AND h.platform IN ('darwin', 'windows', 'ios', 'ipados')`
}
return sql, params
}
@@ -1297,7 +1297,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
// or are servers. Similar logic could be applied to macOS hosts but is not included in this
// current implementation.
- sqlFmt := ` AND h.platform IN('windows', 'darwin')`
+ sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados')`
if opt.TeamFilter == nil {
// OS settings filter is not compatible with the "all teams" option so append the "no team"
// filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
@@ -1306,7 +1306,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
var whereMacOS, whereWindows string
sqlFmt += `
AND ((h.platform = 'windows' AND (%s))
-OR (h.platform = 'darwin' AND (%s)))`
+OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)))`
whereMacOS, paramsMacOS, err := subqueryOSSettingsStatusMac()
if err != nil {
@@ -1741,8 +1741,8 @@ func matchHostDuringEnrollment(ctx context.Context, q sqlx.QueryerContext, enrol
if query.Len() > 0 {
_, _ = query.WriteString(" UNION ")
}
- _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 2 priority FROM hosts WHERE hardware_serial = ? AND platform = ? ORDER BY id LIMIT 1)`)
- args = append(args, serial, "darwin")
+ _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 2 priority FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`)
+ args = append(args, serial)
}
if err := sqlx.SelectContext(ctx, q, &rows, query.String(), args...); err != nil {
@@ -3814,7 +3814,8 @@ func (ds *Datastore) GetHostMDMCheckinInfo(ctx context.Context, hostUUID string)
COALESCE(h.team_id, 0) as team_id,
hda.host_id IS NOT NULL AND hda.deleted_at IS NULL as dep_assigned_to_fleet,
h.node_key IS NOT NULL as osquery_enrolled,
- ncaa.renew_command_uuid IS NOT NULL as scep_renewal_in_progress
+ ncaa.renew_command_uuid IS NOT NULL as scep_renewal_in_progress,
+ h.platform
FROM
hosts h
LEFT JOIN
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index a829e8ee89..26f8dc41c8 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -7391,6 +7391,7 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) {
PrimaryMac: "30-65-EC-6F-C4-58",
HardwareSerial: "123456789",
TeamID: &tm.ID,
+ Platform: "darwin",
})
require.NoError(t, err)
err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://fleetdm.com", true, fleet.WellKnownMDMFleet, "")
@@ -7403,6 +7404,7 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) {
require.EqualValues(t, tm.ID, info.TeamID)
require.False(t, info.DEPAssignedToFleet)
require.True(t, info.OsqueryEnrolled)
+ require.Equal(t, "darwin", info.Platform)
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*host})
require.NoError(t, err)
diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go
index aff32bdd18..c8c137fd43 100644
--- a/server/datastore/mysql/mdm.go
+++ b/server/datastore/mysql/mdm.go
@@ -425,7 +425,7 @@ FROM hosts h
JOIN mdm_apple_configuration_profiles macp
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
WHERE
- macp.profile_uuid IN (?) AND h.platform = 'darwin'`
+ macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')`
args = append(args, macProfUUIDs)
case len(winProfUUIDs) > 0:
@@ -454,12 +454,12 @@ WHERE
}
}
- var macHosts []string
+ var appleHosts []string
var winHosts []string
for _, h := range hosts {
switch h.Platform {
- case "darwin":
- macHosts = append(macHosts, h.UUID)
+ case "darwin", "ios", "ipados":
+ appleHosts = append(appleHosts, h.UUID)
case "windows":
winHosts = append(winHosts, h.UUID)
default:
@@ -471,7 +471,7 @@ WHERE
}
}
- if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, macHosts); err != nil {
+ if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts); err != nil {
return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
}
@@ -537,7 +537,7 @@ WHERE
var stmt string
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
@@ -577,7 +577,7 @@ WHERE
var stmt string
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
@@ -630,7 +630,7 @@ WHERE
var stmt string
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
@@ -667,7 +667,7 @@ func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Conte
}
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID, host.ID)
case "windows":
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID, host.ID)
@@ -823,7 +823,7 @@ WHERE
var stmt string
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
@@ -860,7 +860,7 @@ WHERE
var stmt string
switch host.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go
index 4ecf682f15..775a0c4948 100644
--- a/server/datastore/mysql/scripts.go
+++ b/server/datastore/mysql/scripts.go
@@ -706,7 +706,7 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, host *fleet.Host
}
switch fleetPlatform {
- case "darwin":
+ case "darwin", "ios", "ipados":
if mdmActions.UnlockPIN != nil {
status.UnlockPIN = *mdmActions.UnlockPIN
}
diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go
index ff4a041494..5691650265 100644
--- a/server/fleet/apple_mdm.go
+++ b/server/fleet/apple_mdm.go
@@ -255,7 +255,7 @@ type HostMDMAppleProfile struct {
}
// ToHostMDMProfile converts the HostMDMAppleProfile to a HostMDMProfile.
-func (p HostMDMAppleProfile) ToHostMDMProfile() HostMDMProfile {
+func (p HostMDMAppleProfile) ToHostMDMProfile(platform string) HostMDMProfile {
return HostMDMProfile{
HostUUID: p.HostUUID,
ProfileUUID: p.ProfileUUID,
@@ -264,7 +264,7 @@ func (p HostMDMAppleProfile) ToHostMDMProfile() HostMDMProfile {
Status: p.Status,
OperationType: p.OperationType,
Detail: p.Detail,
- Platform: "darwin",
+ Platform: platform,
}
}
@@ -292,6 +292,7 @@ type MDMAppleProfilePayload struct {
ProfileIdentifier string `db:"profile_identifier"`
ProfileName string `db:"profile_name"`
HostUUID string `db:"host_uuid"`
+ HostPlatform string `db:"host_platform"`
Checksum []byte `db:"checksum"`
Status *MDMDeliveryStatus `db:"status" json:"status"`
OperationType MDMOperationType `db:"operation_type"`
diff --git a/server/fleet/cron_schedules.go b/server/fleet/cron_schedules.go
index 6b16734fd4..f6d7173ebe 100644
--- a/server/fleet/cron_schedules.go
+++ b/server/fleet/cron_schedules.go
@@ -12,16 +12,17 @@ type CronScheduleName string
// List of recognized cron schedule names.
const (
- CronAppleMDMDEPProfileAssigner CronScheduleName = "apple_mdm_dep_profile_assigner"
- CronCleanupsThenAggregation CronScheduleName = "cleanups_then_aggregation"
- CronFrequentCleanups CronScheduleName = "frequent_cleanups"
- CronUsageStatistics CronScheduleName = "usage_statistics"
- CronVulnerabilities CronScheduleName = "vulnerabilities"
- CronAutomations CronScheduleName = "automations"
- CronWorkerIntegrations CronScheduleName = "integrations"
- CronActivitiesStreaming CronScheduleName = "activities_streaming"
- CronMDMAppleProfileManager CronScheduleName = "mdm_apple_profile_manager"
- CronCalendar CronScheduleName = "calendar"
+ CronAppleMDMDEPProfileAssigner CronScheduleName = "apple_mdm_dep_profile_assigner"
+ CronCleanupsThenAggregation CronScheduleName = "cleanups_then_aggregation"
+ CronFrequentCleanups CronScheduleName = "frequent_cleanups"
+ CronUsageStatistics CronScheduleName = "usage_statistics"
+ CronVulnerabilities CronScheduleName = "vulnerabilities"
+ CronAutomations CronScheduleName = "automations"
+ CronWorkerIntegrations CronScheduleName = "integrations"
+ CronActivitiesStreaming CronScheduleName = "activities_streaming"
+ CronMDMAppleProfileManager CronScheduleName = "mdm_apple_profile_manager"
+ CronAppleMDMIPhoneIPadRefetcher CronScheduleName = "apple_mdm_iphone_ipad_refetcher"
+ CronCalendar CronScheduleName = "calendar"
)
type CronSchedulesService interface {
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index c32f9b8b92..0f8cf591e1 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -325,6 +325,10 @@ type Datastore interface {
GetHostMDM(ctx context.Context, hostID uint) (*HostMDM, error)
GetHostMDMCheckinInfo(ctx context.Context, hostUUID string) (*HostMDMCheckinInfo, error)
+ // ListIOSAndIPadOSToRefetch returns the UUIDs of iPhones/iPads that should be refetched (their details haven't been
+ // updated in the given `interval`).
+ ListIOSAndIPadOSToRefetch(ctx context.Context, refetchInterval time.Duration) (uuids []string, err error)
+
AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, time.Time, error)
AggregatedMunkiIssues(ctx context.Context, teamID *uint) ([]AggregatedMunkiIssue, time.Time, error)
AggregatedMDMStatus(ctx context.Context, teamID *uint, platform string) (AggregatedMDMStatus, time.Time, error)
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 1c3bad6e6e..05b879786a 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -840,6 +840,11 @@ func (h *Host) FleetPlatform() string {
return PlatformFromHost(h.Platform)
}
+// SupportsOsquery returns whether the device runs osquery.
+func (h *Host) SupportsOsquery() bool {
+ return h.Platform != "ios" && h.Platform != "ipados"
+}
+
// HostLinuxOSs are the possible linux values for Host.Platform.
var HostLinuxOSs = []string{
"linux", "ubuntu", "debian", "rhel", "centos", "sles", "kali", "gentoo", "amzn", "pop", "arch", "linuxmint", "void", "nixos", "endeavouros", "manjaro", "opensuse-leap", "opensuse-tumbleweed",
@@ -879,7 +884,9 @@ func PlatformFromHost(hostPlatform string) string {
// TODO remove this once that customer migrates to Fleetd for Chrome
hostPlatform == "CrOS",
// Fleet now supports Chrome via fleetd
- hostPlatform == "chrome":
+ hostPlatform == "chrome",
+ hostPlatform == "ios",
+ hostPlatform == "ipados":
return hostPlatform
default:
return ""
@@ -1238,13 +1245,15 @@ type EnrollHostLimiter interface {
}
type HostMDMCheckinInfo struct {
- HardwareSerial string `json:"hardware_serial" db:"hardware_serial"`
- InstalledFromDEP bool `json:"installed_from_dep" db:"installed_from_dep"`
- DisplayName string `json:"display_name" db:"display_name"`
- TeamID uint `json:"team_id" db:"team_id"`
- DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"`
- OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"`
+ HardwareSerial string `json:"hardware_serial" db:"hardware_serial"`
+ InstalledFromDEP bool `json:"installed_from_dep" db:"installed_from_dep"`
+ DisplayName string `json:"display_name" db:"display_name"`
+ TeamID uint `json:"team_id" db:"team_id"`
+ DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"`
+ OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"`
+
SCEPRenewalInProgress bool `json:"-" db:"scep_renewal_in_progress"`
+ Platform string `json:"-" db:"platform"`
}
type HostDiskEncryptionKey struct {
diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go
index 1d1f3c078c..c1ef630e7d 100644
--- a/server/fleet/mdm.go
+++ b/server/fleet/mdm.go
@@ -7,6 +7,8 @@ import (
"fmt"
"net/url"
"time"
+
+ mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
)
const (
@@ -169,6 +171,8 @@ type CommandEnqueueResult struct {
// FailedUUIDs is the list of host UUIDs that failed to receive the command.
FailedUUIDs []string `json:"failed_uuids,omitempty"`
// Platform is the platform of the hosts targeted by the command.
+ // Current possible values are "darwin" or "windows".
+ // Here "darwin" means "Apple" devices (iOS/iPadOS/macOS).
Platform string `json:"platform"`
}
@@ -532,3 +536,42 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool {
return len(pathLabelCounts) == 0
}
+
+// MDMPlatform returns "darwin" or "windows" as MDM platforms
+// derived from a host's platform (hosts.platform field).
+//
+// Note that "darwin" as MDM platform means Apple (we keep it as "darwin"
+// to keep backwards compatibility throughout the app).
+func MDMPlatform(hostPlatform string) string {
+ switch hostPlatform {
+ case "darwin", "ios", "ipados":
+ return "darwin"
+ case "windows":
+ return "windows"
+ }
+ return ""
+}
+
+// MDMSupported returns whether MDM is supported for a given host platform.
+func MDMSupported(hostPlatform string) bool {
+ return MDMPlatform(hostPlatform) != ""
+}
+
+// FilterMacOSOnlyProfilesFromIOSIPadOS will filter out profiles that are only for macOS devices
+// if the profile target's platform is ios/ipados.
+func FilterMacOSOnlyProfilesFromIOSIPadOS(profiles []*MDMAppleProfilePayload) []*MDMAppleProfilePayload {
+ i := 0
+ for _, profilePayload := range profiles {
+ if (profilePayload.HostPlatform == "ios" || profilePayload.HostPlatform == "ipados") &&
+ (profilePayload.ProfileName == mdm_types.FleetdConfigProfileName ||
+ profilePayload.ProfileName == mdm_types.FleetFileVaultProfileName) {
+ continue
+ }
+ profiles[i] = profilePayload
+ i++
+ }
+ return profiles[:i]
+}
+
+// RefetchCommandUUIDPrefix is the prefix used for MDM commands used to refetch information from iOS/iPadOS devices.
+const RefetchCommandUUIDPrefix = "REFETCH-"
diff --git a/server/fleet/mdm_test.go b/server/fleet/mdm_test.go
index 184649514b..e35099f551 100644
--- a/server/fleet/mdm_test.go
+++ b/server/fleet/mdm_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
+ fleetmdm "github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
@@ -324,3 +325,146 @@ func TestMDMProfileSpecsMatch(t *testing.T) {
})
}
}
+
+func TestFilterMacOSOnlyProfilesFromIOSIPadOS(t *testing.T) {
+ for _, tc := range []struct {
+ profiles []*fleet.MDMAppleProfilePayload
+ expectedProfiles []*fleet.MDMAppleProfilePayload
+ }{
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{},
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{},
+ },
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: "SomeProfile",
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: fleetmdm.FleetdConfigProfileName,
+ HostPlatform: "ipados",
+ },
+ {
+ ProfileName: fleetmdm.FleetdConfigProfileName,
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: "SomeProfile2",
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: "SomeProfile3",
+ HostPlatform: "ipados",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ipados",
+ },
+ },
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: "SomeProfile",
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: "SomeProfile2",
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: "SomeProfile3",
+ HostPlatform: "ipados",
+ },
+ },
+ },
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: "SomeProfile",
+ HostPlatform: "ios",
+ },
+ },
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: "SomeProfile",
+ HostPlatform: "ios",
+ },
+ },
+ },
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ipados",
+ },
+ },
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{},
+ },
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ipados",
+ },
+ },
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{},
+ },
+ {
+ profiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ios",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "ipados",
+ },
+ },
+ expectedProfiles: []*fleet.MDMAppleProfilePayload{
+ {
+ ProfileName: fleetmdm.FleetFileVaultProfileName,
+ HostPlatform: "darwin",
+ },
+ },
+ },
+ } {
+ actualProfiles := fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(tc.profiles)
+ require.Equal(t, len(actualProfiles), len(tc.expectedProfiles))
+ for i := 0; i < len(actualProfiles); i++ {
+ require.Equal(t, *actualProfiles[i], *tc.expectedProfiles[i])
+ }
+
+ }
+}
diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go
index 2fd7ece7f4..5587e71f4b 100644
--- a/server/mdm/lifecycle/lifecycle.go
+++ b/server/mdm/lifecycle/lifecycle.go
@@ -58,9 +58,9 @@ func New(ds fleet.Datastore, logger kitlog.Logger) *HostLifecycle {
// Do executes the provided HostAction based on the platform requested
func (t *HostLifecycle) Do(ctx context.Context, opts HostOptions) error {
switch opts.Platform {
- case "darwin":
+ case "darwin", "ios", "ipados":
err := t.doDarwin(ctx, opts)
- return ctxerr.Wrapf(ctx, err, "running darwin lifecycle action %s", opts.Action)
+ return ctxerr.Wrapf(ctx, err, "running apple lifecycle action %s", opts.Action)
case "windows":
err := t.doWindows(ctx, opts)
return ctxerr.Wrapf(ctx, err, "running windows lifecycle action %s", opts.Action)
@@ -124,6 +124,7 @@ func (t *HostLifecycle) resetDarwin(ctx context.Context, opts HostOptions) error
UUID: opts.UUID,
HardwareSerial: opts.HardwareSerial,
HardwareModel: opts.HardwareModel,
+ Platform: opts.Platform,
}
if err := t.ds.MDMAppleUpsertHost(ctx, host); err != nil {
return ctxerr.Wrap(ctx, err, "upserting mdm host")
@@ -170,6 +171,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
t.logger,
worker.AppleMDMPostDEPEnrollmentTask,
opts.UUID,
+ opts.Platform,
tmID,
opts.EnrollReference,
)
@@ -184,6 +186,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
t.logger,
worker.AppleMDMPostManualEnrollmentTask,
opts.UUID,
+ opts.Platform,
tmID,
opts.EnrollReference,
); err != nil {
diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go
index 8439070714..041aeb960d 100644
--- a/server/mdm/mdm.go
+++ b/server/mdm/mdm.go
@@ -38,7 +38,7 @@ func prefixMatches(val []byte, prefix string) bool {
// GetRawProfilePlatform identifies the platform type of a profile bytes by
// examining its initial content:
//
-// - Returns "darwin" if the profile starts with "
+
+
+
+ CommandUUID
+ REFETCH-fd23f8ac-1c50-41c7-a5bb-f13633c9ea97
+ QueryResponses
+
+ AvailableDeviceCapacity
+ 51.260395520000003
+ DeviceCapacity
+ 64
+ DeviceName
+ Work iPad
+ OSVersion
+ 17.5.1
+ ProductName
+ iPad13,18
+ WiFiMAC
+ ff:ff:ff:ff:ff:ff
+
+ Status
+ Acknowledged
+ UDID
+ FFFFFFFF-FFFFFFFFFFFFFFFF
+
+`),
+ },
+ )
+ require.NoError(t, err)
+
+ require.True(t, ds.UpdateHostFuncInvoked)
+ require.True(t, ds.HostByIdentifierFuncInvoked)
+ require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked)
+}
diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go
index 4eb82d0968..a61ef3fd6e 100644
--- a/server/service/client_mdm.go
+++ b/server/service/client_mdm.go
@@ -287,7 +287,7 @@ func (c *Client) RunMDMCommand(hostUUIDs []string, rawCmd []byte, forPlatform st
case "windows":
prepareFn = c.prepareWindowsMDMCommand
default:
- return nil, fmt.Errorf("Invalid platform %q. You can only run MDM commands on Windows or macOS hosts.", forPlatform)
+ return nil, fmt.Errorf("Invalid platform %q. You can only run MDM commands on Windows or Apple hosts.", forPlatform)
}
rawCmd, err := prepareFn(rawCmd)
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 62dbab643c..dacd56ddc2 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -293,7 +293,7 @@ func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, filter *map[str
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger)
for _, host := range hosts {
- if host.Platform == "darwin" || host.Platform == "windows" {
+ if fleet.MDMSupported(host.Platform) {
err := mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
Action: mdmlifecycle.HostActionDelete,
Host: host,
@@ -749,7 +749,7 @@ func (svc *Service) DeleteHost(ctx context.Context, id uint) error {
return ctxerr.Wrap(ctx, err, "delete host")
}
- if host.Platform == "windows" || host.Platform == "darwin" {
+ if fleet.MDMSupported(host.Platform) {
mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger)
err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{
Action: mdmlifecycle.HostActionDelete,
@@ -1104,7 +1104,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
profiles = append(profiles, p.ToHostMDMProfile())
}
- case "darwin":
+ case "darwin", "ios", "ipados":
if ac.MDM.EnabledAndConfigured {
profs, err := svc.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
if err != nil {
@@ -1120,7 +1120,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
p.Status = host.MDM.ProfileStatusFromDiskEncryptionState(p.Status)
}
p.Detail = fleet.HostMDMProfileDetail(p.Detail).Message()
- profiles = append(profiles, p.ToHostMDMProfile())
+ profiles = append(profiles, p.ToHostMDMProfile(host.Platform))
}
}
}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 7be3faed47..5347ca84bd 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -6951,7 +6951,6 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() {
assertVersionsResp(versResp, nil, time.Time{}, "", expectedVulnVersionsCount)
// /software/versions filtered by name, version, cve (`/software` is deprecated)
- // TODO(jacob) use `assertVersionsResp`
versionsResp := listSoftwareVersionsResponse{}
s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Name)
assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1)
diff --git a/server/service/mdm.go b/server/service/mdm.go
index adbd2c0c13..dc81199de4 100644
--- a/server/service/mdm.go
+++ b/server/service/mdm.go
@@ -496,8 +496,8 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host
for platform := range platforms {
commandPlatform = platform
}
- if commandPlatform != "windows" && commandPlatform != "darwin" {
- err := fleet.NewInvalidArgumentError("host_uuids", "Invalid platform. You can only run MDM commands on Windows or macOS hosts.")
+ if !fleet.MDMSupported(commandPlatform) {
+ err := fleet.NewInvalidArgumentError("host_uuids", "Invalid platform. You can only run MDM commands on Windows or Apple hosts.")
return nil, ctxerr.Wrap(ctx, err, "check host platform")
}
@@ -2038,7 +2038,7 @@ func (svc *Service) ResendHostMDMProfile(ctx context.Context, hostID uint, profi
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest), "check apple mdm enabled")
}
- if host.Platform != "darwin" {
+ if host.Platform != "darwin" && host.Platform != "ios" && host.Platform != "ipados" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", "Profile is not compatible with host platform."), "check host platform")
}
prof, err := svc.ds.GetMDMAppleConfigProfile(ctx, profileUUID)
@@ -2052,7 +2052,7 @@ func (svc *Service) ResendHostMDMProfile(ctx context.Context, hostID uint, profi
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest), "check apple mdm enabled")
}
- if host.Platform != "darwin" {
+ if host.Platform != "darwin" && host.Platform != "ios" && host.Platform != "ipados" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", "Profile is not compatible with host platform."), "check host platform")
}
decl, err := svc.ds.GetMDMAppleDeclaration(ctx, profileUUID)
diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go
index 0537af05e8..35555a9ea5 100644
--- a/server/worker/apple_mdm.go
+++ b/server/worker/apple_mdm.go
@@ -50,6 +50,7 @@ type appleMDMArgs struct {
TeamID *uint `json:"team_id,omitempty"`
EnrollReference string `json:"enroll_reference,omitempty"`
EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
+ Platform string `json:"platform,omitempty"`
}
// Run executes the apple_mdm job.
@@ -83,9 +84,17 @@ func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error {
}
}
+func isMacOS(platform string) bool {
+ // For backwards compatibility, we assume empty platform in job arguments is macOS.
+ return platform == "" ||
+ platform == "darwin"
+}
+
func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error {
- if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
- return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
+ if isMacOS(args.Platform) {
+ if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
+ return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
+ }
}
return nil
@@ -94,18 +103,20 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg
func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error {
var awaitCmdUUIDs []string
- fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
- }
- awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID)
+ if isMacOS(args.Platform) {
+ fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
+ }
+ awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID)
- bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
- }
- if bootstrapCmdUUID != "" {
- awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
+ bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
+ }
+ if bootstrapCmdUUID != "" {
+ awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
+ }
}
if ref := args.EnrollReference; ref != "" {
@@ -166,7 +177,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
// be final and same for MDM profiles of that host; it means the DEP
// enrollment process is done and the device can be released.
if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
- args.HostUUID, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
+ args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
}
}
@@ -323,6 +334,7 @@ func QueueAppleMDMJob(
logger kitlog.Logger,
task AppleMDMTask,
hostUUID string,
+ platform string,
teamID *uint,
enrollReference string,
enrollmentCommandUUIDs ...string,
@@ -331,13 +343,14 @@ func QueueAppleMDMJob(
"enabled", "true",
appleMDMJobName, task,
"host_uuid", hostUUID,
+ "platform", platform,
"with_enroll_reference", enrollReference != "",
}
if teamID != nil {
attrs = append(attrs, "team_id", *teamID)
}
if len(enrollmentCommandUUIDs) > 0 {
- attrs = append(attrs, "enrollment_commands", enrollmentCommandUUIDs)
+ attrs = append(attrs, "enrollment_commands", fmt.Sprintf("%v", enrollmentCommandUUIDs))
}
level.Info(logger).Log(attrs...)
@@ -347,6 +360,7 @@ func QueueAppleMDMJob(
TeamID: teamID,
EnrollReference: enrollReference,
EnrollmentCommands: enrollmentCommandUUIDs,
+ Platform: platform,
}
// the release device task is always added with a delay
diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go
index fd42b97027..f1809be57a 100644
--- a/server/worker/apple_mdm_test.go
+++ b/server/worker/apple_mdm_test.go
@@ -130,7 +130,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should mark the job as done
@@ -159,7 +159,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should mark the job as failed
@@ -190,7 +190,8 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "")
+ // use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support.
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "")
require.NoError(t, err)
// run the worker, should succeed
@@ -227,7 +228,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should succeed
@@ -268,7 +269,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should succeed
@@ -319,7 +320,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
require.NoError(t, err)
// run the worker, should succeed
@@ -371,7 +372,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
require.NoError(t, err)
// run the worker, should succeed
@@ -408,7 +409,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "abcd")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd")
require.NoError(t, err)
// run the worker, should succeed
@@ -450,7 +451,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, idpAcc.UUID)
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID)
require.NoError(t, err)
// run the worker, should succeed
@@ -505,7 +506,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, idpAcc.UUID)
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID)
require.NoError(t, err)
// run the worker, should succeed
@@ -541,7 +542,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "")
require.NoError(t, err)
// run the worker, should succeed