diff --git a/changes/feature_19010-ipad-ios-wipe b/changes/feature_19010-ipad-ios-wipe
new file mode 100644
index 0000000000..872132eea9
--- /dev/null
+++ b/changes/feature_19010-ipad-ios-wipe
@@ -0,0 +1 @@
+* Added support to wipe iOS/iPadOS devices.
diff --git a/cmd/fleetctl/mdm.go b/cmd/fleetctl/mdm.go
index b21231dd9f..fabd2539b7 100644
--- a/cmd/fleetctl/mdm.go
+++ b/cmd/fleetctl/mdm.go
@@ -316,7 +316,6 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl
if err != nil {
var nfe service.NotFoundErr
if errors.As(err, &nfe) {
- fmt.Println(hostIdent)
return nil, nil, errors.New("The host doesn't exist. Please provide a valid host identifier.")
}
diff --git a/ee/server/service/hosts.go b/ee/server/service/hosts.go
index a4c0a5bd06..e73ae027a4 100644
--- a/ee/server/service/hosts.go
+++ b/ee/server/service/hosts.go
@@ -58,6 +58,8 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) (unlockPIN string
// locking validations are based on the platform of the host
switch host.FleetPlatform() {
+ case "ios", "ipados":
+ return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock iOS or iPadOS hosts. Use wipe instead."))
case "darwin":
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
if errors.Is(err, fleet.ErrMDMNotConfigured) {
@@ -176,7 +178,7 @@ func (svc *Service) UnlockHost(ctx context.Context, hostID uint) (string, error)
// locking validations are based on the platform of the host
switch host.FleetPlatform() {
- case "darwin":
+ case "darwin", "ios", "ipados":
// all good, no need to check if MDM enrolled, will validate later that it
// is currently locked.
@@ -267,7 +269,7 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
// uses scripts, not MDM.
var requireMDM bool
switch host.FleetPlatform() {
- case "darwin":
+ case "darwin", "ios", "ipados":
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
if errors.Is(err, fleet.ErrMDMNotConfigured) {
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
@@ -471,7 +473,7 @@ func (svc *Service) enqueueWipeHostRequest(ctx context.Context, host *fleet.Host
}
switch wipeStatus.HostFleetPlatform {
- case "darwin":
+ case "darwin", "ios", "ipados":
wipeCommandUUID := uuid.NewString()
if err := svc.mdmAppleCommander.EraseDevice(ctx, host, wipeCommandUUID); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing wipe request for darwin")
diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts
index d01e043a09..ec2e11efd3 100644
--- a/frontend/interfaces/platform.ts
+++ b/frontend/interfaces/platform.ts
@@ -68,6 +68,8 @@ export const HOST_LINUX_PLATFORMS = [
"tuxedo",
] as const;
+export const HOST_APPLE_PLATFORMS = ["darwin", "ios", "ipados"] as const;
+
/**
* Checks if the provided platform is a Linux-like OS. We can recieve many
* different types of host platforms so we need a check that will cover all
@@ -78,3 +80,9 @@ export const isLinuxLike = (platform: string) => {
platform as typeof HOST_LINUX_PLATFORMS[number]
);
};
+
+export const isAppleDevice = (platform: string) => {
+ return HOST_APPLE_PLATFORMS.includes(
+ platform as typeof HOST_APPLE_PLATFORMS[number]
+ );
+};
diff --git a/frontend/interfaces/script.ts b/frontend/interfaces/script.ts
index 0d3acce6e3..67d9572052 100644
--- a/frontend/interfaces/script.ts
+++ b/frontend/interfaces/script.ts
@@ -9,7 +9,7 @@ export interface IScript {
}
export const isScriptSupportedPlatform = (hostPlatform: string) =>
- ["darwin", "windows", ...HOST_LINUX_PLATFORMS].includes(hostPlatform); // excludes chrome, see also https://github.com/fleetdm/fleet/blob/5a21e2cfb029053ddad0508869eb9f1f23997bf2/server/fleet/hosts.go#L775
+ ["darwin", "windows", ...HOST_LINUX_PLATFORMS].includes(hostPlatform); // excludes chrome, ios, ipados see also https://github.com/fleetdm/fleet/blob/5a21e2cfb029053ddad0508869eb9f1f23997bf2/server/fleet/hosts.go#L775
export type IScriptExecutionStatus = "ran" | "pending" | "error";
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
index 82a7f85dac..dbde20119a 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
@@ -785,7 +785,7 @@ describe("Host Actions Dropdown", () => {
expect(screen.queryByText("Unlock")).not.toBeInTheDocument();
});
- it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
+ it("does not renders when a macOS host but does not have Fleet mac mdm enabled and configured", async () => {
const render = createCustomRenderer({
context: {
app: {
@@ -921,7 +921,7 @@ describe("Host Actions Dropdown", () => {
expect(screen.queryByText("Wipe")).not.toBeInTheDocument();
});
- it("does not renders when a mac host but does not have Fleet mac mdm enabled and configured", async () => {
+ it("does not renders when a macOS host but does not have Fleet macOS mdm enabled and configured", async () => {
const render = createCustomRenderer({
context: {
app: {
@@ -1139,55 +1139,85 @@ describe("Host Actions Dropdown", () => {
});
});
- describe("Does not render dropdown for certain platforms", () => {
- it("does not render dropdown for iOS", async () => {
+ describe("Render options only available for iOS and iPadOS", () => {
+ it("renders only the transfer, wipe, and delete options for iOS", async () => {
const render = createCustomRenderer({
context: {
app: {
+ isPremiumTier: true,
isGlobalAdmin: true,
+ isMacMdmEnabledAndConfigured: true,
currentUser: createMockUser(),
},
},
});
- render(
+ const { user } = render(
);
- expect(screen.queryByText("Actions")).not.toBeInTheDocument();
+ await user.click(screen.getByText("Actions"));
+
+ expect(screen.queryByText("Transfer")).toBeInTheDocument();
+ expect(screen.queryByText("Wipe")).toBeInTheDocument();
+ expect(screen.queryByText("Delete")).toBeInTheDocument();
+
+ expect(screen.queryByText("Query")).not.toBeInTheDocument();
+ expect(screen.queryByText("Run script")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText("Show disk encryption key")
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText("Turn off MDM")).not.toBeInTheDocument();
+ expect(screen.queryByText("Lock")).not.toBeInTheDocument();
});
- it("does not render dropdown for iPadOS", async () => {
+ it("renders only the transfer, wipe, and delete options for iPadOS", async () => {
const render = createCustomRenderer({
context: {
app: {
+ isPremiumTier: true,
isGlobalAdmin: true,
+ isMacMdmEnabledAndConfigured: true,
currentUser: createMockUser(),
},
},
});
- render(
+ const { user } = render(
);
- expect(screen.queryByText("Actions")).not.toBeInTheDocument();
+ await user.click(screen.getByText("Actions"));
+
+ expect(screen.queryByText("Transfer")).toBeInTheDocument();
+ expect(screen.queryByText("Wipe")).toBeInTheDocument();
+ expect(screen.queryByText("Delete")).toBeInTheDocument();
+
+ expect(screen.queryByText("Query")).not.toBeInTheDocument();
+ expect(screen.queryByText("Run script")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText("Show disk encryption key")
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText("Turn off MDM")).not.toBeInTheDocument();
+ expect(screen.queryByText("Lock")).not.toBeInTheDocument();
});
});
});
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
index 21e5f807c7..9d466d545d 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
@@ -79,10 +79,6 @@ const HostActionsDropdown = ({
// No options to render. Exit early
if (options.length === 0) return null;
- if (hostPlatform === "ios" || hostPlatform === "ipados") {
- return null;
- }
-
return (
{
const canEditMdm = (config: IHostActionConfigOptions) => {
const {
+ hostPlatform,
isGlobalAdmin,
isGlobalMaintainer,
isTeamAdmin,
@@ -95,7 +96,7 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
isMacMdmEnabledAndConfigured,
} = config;
return (
- config.hostPlatform === "darwin" &&
+ hostPlatform === "darwin" &&
isMacMdmEnabledAndConfigured &&
isEnrolledInMdm &&
isFleetMdm &&
@@ -103,6 +104,13 @@ const canEditMdm = (config: IHostActionConfigOptions) => {
);
};
+const canQueryHost = ({ hostPlatform }: IHostActionConfigOptions) => {
+ // Currently we cannot query iOS or iPadOS
+ const isIosOrIpadosHost = hostPlatform === "ios" || hostPlatform === "ipados";
+
+ return !isIosOrIpadosHost;
+};
+
const canLockHost = ({
isPremiumTier,
hostPlatform,
@@ -146,17 +154,18 @@ const canWipeHost = ({
hostMdmDeviceStatus,
}: IHostActionConfigOptions) => {
const hostMdmEnabled =
- (hostPlatform === "darwin" && isMacMdmEnabledAndConfigured) ||
+ (isAppleDevice(hostPlatform) && isMacMdmEnabledAndConfigured) ||
(hostPlatform === "windows" && isWindowsMdmEnabledAndConfigured);
- // macOS and Windows hosts have the same conditions and can be wiped if they
+ // Windows and Apple devices (i.e. macOS, iOS, iPadOS) have the same conditions and can be wiped if they
// are enrolled in MDM and the MDM is enabled.
- const canWipeMacOrWindows = hostMdmEnabled && isFleetMdm && isEnrolledInMdm;
+ const canWipeWindowsOrAppleOS =
+ hostMdmEnabled && isFleetMdm && isEnrolledInMdm;
return (
isPremiumTier &&
hostMdmDeviceStatus === "unlocked" &&
- (isLinuxLike(hostPlatform) || canWipeMacOrWindows) &&
+ (isLinuxLike(hostPlatform) || canWipeWindowsOrAppleOS) &&
(isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer)
);
};
@@ -205,8 +214,12 @@ const canDeleteHost = (config: IHostActionConfigOptions) => {
};
const canShowDiskEncryption = (config: IHostActionConfigOptions) => {
- const { isPremiumTier, doesStoreEncryptionKey } = config;
- return isPremiumTier && doesStoreEncryptionKey;
+ const { isPremiumTier, doesStoreEncryptionKey, hostPlatform } = config;
+
+ // Currently we cannot show disk encryption key for iOS or iPadOS
+ const isIosOrIpadosHost = hostPlatform === "ios" || hostPlatform === "ipados";
+
+ return isPremiumTier && doesStoreEncryptionKey && !isIosOrIpadosHost;
};
const canRunScript = ({
@@ -237,6 +250,10 @@ const removeUnavailableOptions = (
options = options.filter((option) => option.value !== "transfer");
}
+ if (!canQueryHost(config)) {
+ options = options.filter((option) => option.value !== "query");
+ }
+
if (!canShowDiskEncryption(config)) {
options = options.filter((option) => option.value !== "diskEncryption");
}
@@ -266,9 +283,8 @@ const removeUnavailableOptions = (
}
// TODO: refactor to filter in one pass using predefined filters specified for each of the
- // DEFAULT_OPTIONS. Note that as currently, structured the default is to include all options. For
- // example, "Query" is implicitly included by default because there is no equivalent `canQuery`
- // filter being applied here. This is a bit confusing since
+ // DEFAULT_OPTIONS. Note that as currently, structured the default is to include all options.
+ // This is a bit confusing since we remove options instead of add options
return options;
};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 26ac200d74..741249f5cc 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -77,7 +77,7 @@ import TransferHostModal from "../../components/TransferHostModal";
import DeleteHostModal from "../../components/DeleteHostModal";
import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal";
-import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
+import HostActionsDropdown from "./HostActionsDropdown/HostActionsDropdown";
import OSSettingsModal from "../OSSettingsModal";
import BootstrapPackageModal from "./modals/BootstrapPackageModal";
import RunScriptModal from "./modals/RunScriptModal";
@@ -672,7 +672,7 @@ const HostDetailsPage = ({
}
return (
-
- platform === "darwin"
+ isAppleDevice(platform)
? "Host is locked. The end user can’t use the host until the six-digit PIN has been entered."
: "Host is locked. The end user can’t use the host until the host has been unlocked.",
},
@@ -43,7 +44,7 @@ export const DEVICE_STATUS_TAGS: DeviceStatusTagConfig = {
title: "WIPED",
tagType: "error",
generateTooltip: (platform) =>
- platform === "darwin"
+ isAppleDevice(platform)
? "Host is wiped. To prevent the host from automatically reenrolling to Fleet, first release the host from Apple Business Manager and then delete the host in Fleet."
: "Host is wiped.",
},
diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go
index 5497591e4f..bd86ce7532 100644
--- a/server/fleet/scripts.go
+++ b/server/fleet/scripts.go
@@ -303,8 +303,10 @@ const (
)
// anchored, so that it matches to the end of the line
-var scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/z?sh(?:\s*|\s+.*)$`)
-var ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh."`)
+var (
+ scriptHashbangValidation = regexp.MustCompile(`^#!\s*(:?/usr)?/bin/z?sh(?:\s*|\s+.*)$`)
+ ErrUnsupportedInterpreter = errors.New(`Interpreter not supported. Shell scripts must run in "#!/bin/sh" or "#!/bin/zsh."`)
+)
// ValidateShebang validates if we support a script, and whether we
// can execute it directly, or need to pass it to a shell interpreter.
@@ -402,7 +404,7 @@ type HostLockWipeStatus struct {
}
func (s *HostLockWipeStatus) IsPendingLock() bool {
- if s.HostFleetPlatform == "darwin" {
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// pending lock if an MDM command is queued but no result received yet
return s.LockMDMCommand != nil && s.LockMDMCommandResult == nil
}
@@ -411,7 +413,7 @@ func (s *HostLockWipeStatus) IsPendingLock() bool {
}
func (s HostLockWipeStatus) IsPendingUnlock() bool {
- if s.HostFleetPlatform == "darwin" {
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// Apple MDM does not have a concept of pending unlock.
return false
}
@@ -432,7 +434,7 @@ func (s HostLockWipeStatus) IsLocked() bool {
// this state is regardless of pending unlock/wipe (it reports whether the
// host is locked *now*).
- if s.HostFleetPlatform == "darwin" {
+ if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// locked if an MDM command was sent and succeeded
return s.LockMDMCommand != nil && s.LockMDMCommandResult != nil &&
s.LockMDMCommandResult.Status == MDMAppleStatusAcknowledged
@@ -458,7 +460,7 @@ func (s HostLockWipeStatus) IsWiped() bool {
// wiped if an MDM command was sent and succeeded
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
strings.HasPrefix(s.WipeMDMCommandResult.Status, "2")
- case "darwin":
+ case "darwin", "ios", "ipados":
// wiped if an MDM command was sent and succeeded
return s.WipeMDMCommand != nil && s.WipeMDMCommandResult != nil &&
s.WipeMDMCommandResult.Status == MDMAppleStatusAcknowledged
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 9799f6e7e0..23f8d5d673 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -1423,6 +1423,12 @@ func (svc *Service) EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx context.Co
return ctxerr.Wrap(ctx, err, "getting host info for mdm apple remove profile command")
}
+ if h.Platform == "ios" || h.Platform == "ipados" {
+ return &fleet.BadRequestError{
+ Message: "Can't turn off MDM for iOS or iPadOS hosts. Use wipe instead.",
+ }
+ }
+
info, err := svc.ds.GetHostMDMCheckinInfo(ctx, h.UUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting mdm checkin info for mdm apple remove profile command")
diff --git a/tools/mdm/apple/applebmapi/main.go b/tools/mdm/apple/applebmapi/main.go
index ae47ee1902..d1ecd69ed0 100644
--- a/tools/mdm/apple/applebmapi/main.go
+++ b/tools/mdm/apple/applebmapi/main.go
@@ -19,31 +19,25 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
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"
kitlog "github.com/go-kit/kit/log"
)
func main() {
mysqlAddr := flag.String("mysql", "localhost:3306", "mysql address")
- appleBMToken := flag.String("apple-bm-token", "", "path to (decrypted) Apple BM token")
+ serverPrivateKey := flag.String("server-private-key", "", "fleet server's private key (to decrypt MDM assets)")
profileUUID := flag.String("profile-uuid", "", "the Apple profile UUID to retrieve")
serialNum := flag.String("serial-number", "", "serial number of a device to get the device details")
flag.Parse()
- if *appleBMToken == "" {
- log.Fatal("must provide Apple BM token")
+ if *serverPrivateKey == "" {
+ log.Fatal("must provide -server-private-key")
}
if *profileUUID != "" && *serialNum != "" {
log.Fatal("only one of -profile-uuid or -serial-number must be provided")
}
- tok, err := os.ReadFile(*appleBMToken)
- if err != nil {
- log.Fatal(err)
- }
-
cfg := config.MysqlConfig{
Protocol: "tcp",
Address: *mysqlAddr,
@@ -55,17 +49,19 @@ func main() {
ConnMaxLifetime: 0,
}
logger := kitlog.NewLogfmtLogger(os.Stderr)
- opts := []mysql.DBOption{mysql.Logger(logger)}
+ opts := []mysql.DBOption{
+ mysql.Logger(logger),
+ mysql.WithFleetConfig(&config.FleetConfig{
+ Server: config.ServerConfig{
+ PrivateKey: *serverPrivateKey,
+ },
+ }),
+ }
mds, err := mysql.New(cfg, clock.C, opts...)
if err != nil {
log.Fatal(err)
}
- var jsonTok nanodep_client.OAuth1Tokens
- if err := json.Unmarshal(tok, &jsonTok); err != nil {
- log.Fatal(err)
- }
-
depStorage, err := mds.NewMDMAppleDEPStorage()
if err != nil {
log.Fatal(err)