diff --git a/changes/38790-ios-ipados-managed-app-configuration b/changes/38790-ios-ipados-managed-app-configuration new file mode 100644 index 0000000000..d11fa7d8c5 --- /dev/null +++ b/changes/38790-ios-ipados-managed-app-configuration @@ -0,0 +1 @@ +- Added managed app configuration for iOS and iPadOS apps (VPP and in-house), configurable via UI, REST API, and GitOps, with `$FLEET_VAR_*` substitution. diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tests.ts b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tests.ts index fb74f0b71e..08acfa6644 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tests.ts +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tests.ts @@ -97,6 +97,112 @@ describe("getErrorMessage", () => { "Couldn't update configuration. Please try again." ); }); + + it("returns 'doesn't exist' for an unknown $FLEET_VAR_ variable", () => { + const err = { + response: { + data: { + errors: [ + { + name: "configuration", + reason: "unsupported variable $FLEET_VAR_BLA_BLA", + }, + ], + }, + }, + }; + expect(getErrorMessage(err, true)).toBe( + `Couldn't edit. Variable "$FLEET_VAR_BLA_BLA" doesn't exist.` + ); + }); + + it.each([ + ["NDES_SCEP_CHALLENGE"], + ["NDES_SCEP_PROXY_URL"], + ["DIGICERT_DATA_myCA"], + ["DIGICERT_PASSWORD_myCA"], + ["SCEP_WINDOWS_CERTIFICATE_ID"], + ["SMALLSTEP_SCEP_CHALLENGE_myCA"], + ["SMALLSTEP_SCEP_PROXY_URL_myCA"], + ["CUSTOM_SCEP_CHALLENGE_myCA"], + ["CUSTOM_SCEP_PROXY_URL_myCA"], + ["SCEP_RENEWAL_ID"], + ])( + "returns 'isn't supported in managed configuration' for %s", + (varSuffix) => { + const err = { + response: { + data: { + errors: [ + { + name: "configuration", + reason: `unsupported variable $FLEET_VAR_${varSuffix}`, + }, + ], + }, + }, + }; + expect(getErrorMessage(err, true)).toBe( + `Couldn't edit. Variable "$FLEET_VAR_${varSuffix}" isn't supported in managed configuration. It can only be used in configuration profiles.` + ); + } + ); + + it("returns 'doesn't exist' for a typo like $FLEET_VAR_NDES_SCEP_FOO (not a real profile variable)", () => { + const err = { + response: { + data: { + errors: [ + { + name: "configuration", + reason: "unsupported variable $FLEET_VAR_NDES_SCEP_FOO", + }, + ], + }, + }, + }; + expect(getErrorMessage(err, true)).toBe( + `Couldn't edit. Variable "$FLEET_VAR_NDES_SCEP_FOO" doesn't exist.` + ); + }); + + it("returns 'doesn't exist' for a missing $FLEET_SECRET_ variable", () => { + const err = { + response: { + data: { + errors: [ + { + name: "configuration", + reason: + 'Couldn\'t add. Secret variable "$FLEET_SECRET_BLA_BLA" missing from database', + }, + ], + }, + }, + }; + expect(getErrorMessage(err, true)).toBe( + `Couldn't edit. Variable "$FLEET_SECRET_BLA_BLA" doesn't exist.` + ); + }); + + it("returns 'doesn't exist' listing all variables for multiple missing $FLEET_SECRET_ variables", () => { + const err = { + response: { + data: { + errors: [ + { + name: "configuration", + reason: + 'Couldn\'t add. Secret variables "$FLEET_SECRET_A", "$FLEET_SECRET_B" missing from database', + }, + ], + }, + }, + }; + expect(getErrorMessage(err, true)).toBe( + `Couldn't edit. Variables "$FLEET_SECRET_A", "$FLEET_SECRET_B" don't exist.` + ); + }); }); describe("validateJson", () => { diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tsx index a225facb86..a68fdd9c97 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/helpers.tsx @@ -5,6 +5,54 @@ import { getErrorReason } from "interfaces/errors"; const DEFAULT_ERROR_MESSAGE = "Couldn't update configuration. Please try again."; +/** Variables that are valid in configuration profiles but NOT in managed configuration. */ +const PROFILE_ONLY_VARIABLE_PATTERNS = [ + /^NDES_SCEP_(CHALLENGE|PROXY_URL)$/, + /^CUSTOM_SCEP_(CHALLENGE|PROXY_URL)_/, + /^SCEP_RENEWAL_ID$/, + /^DIGICERT_(DATA|PASSWORD)_/, + /^SCEP_WINDOWS_CERTIFICATE_ID$/, + /^SMALLSTEP_SCEP_(CHALLENGE|PROXY_URL)_/, +]; + +const isProfileOnlyVariable = (varNameWithoutPrefix: string): boolean => { + return PROFILE_ONLY_VARIABLE_PATTERNS.some((pattern) => + pattern.test(varNameWithoutPrefix) + ); +}; + +const generateUnsupportedVariableErrMsg = (errMsg: string) => { + const match = errMsg.match(/\$FLEET_VAR_(\w+)/); + if (!match) { + return DEFAULT_ERROR_MESSAGE; + } + const fullVarName = match[0]; + const varNameWithoutPrefix = match[1]; + + if (isProfileOnlyVariable(varNameWithoutPrefix)) { + return `Couldn't edit. Variable "${fullVarName}" isn't supported in managed configuration. It can only be used in configuration profiles.`; + } + + return `Couldn't edit. Variable "${fullVarName}" doesn't exist.`; +}; + +const generateMissingSecretErrMsg = (errMsg: string) => { + const regex = /"\$FLEET_SECRET_\w+"/g; + const varNames: string[] = []; + let m = regex.exec(errMsg); + while (m) { + varNames.push(m[0].replace(/"/g, "")); + m = regex.exec(errMsg); + } + if (varNames.length === 0) { + return DEFAULT_ERROR_MESSAGE; + } + const plural = varNames.length > 1 ? "s" : ""; + const verb = varNames.length > 1 ? "don't" : "doesn't"; + const quoted = varNames.map((v) => `"${v}"`).join(", "); + return `Couldn't edit. Variable${plural} ${quoted} ${verb} exist.`; +}; + export const getErrorMessage = (err: unknown, isApplePlatform: boolean) => { const reason = getErrorReason(err); @@ -23,6 +71,26 @@ export const getErrorMessage = (err: unknown, isApplePlatform: boolean) => { ); } + // Fleet variable ($FLEET_VAR_) unsupported in managed configuration. + // Note: the backend validates $FLEET_VAR_ variables one at a time and + // returns on the first unsupported one it finds, so only one variable is + // surfaced per request even if the configuration contains multiple invalid + // variables. $FLEET_SECRET_ errors can contain multiple variables. + if ( + reason.includes("unsupported variable") && + reason.includes("$FLEET_VAR_") + ) { + return generateUnsupportedVariableErrMsg(reason); + } + + // Secret variable missing from database + if ( + reason.includes("missing from database") && + reason.includes("$FLEET_SECRET_") + ) { + return generateMissingSecretErrMsg(reason); + } + return reason || DEFAULT_ERROR_MESSAGE; }; diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tests.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tests.tsx new file mode 100644 index 0000000000..33fba79de8 --- /dev/null +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tests.tsx @@ -0,0 +1,59 @@ +import { getInstallErrorMessage } from "./helpers"; + +const makeErr = (reason: string) => ({ + response: { + data: { + errors: [{ name: "base", reason }], + }, + }, +}); + +describe("getInstallErrorMessage", () => { + it("returns fleetd-specific message", () => { + const result = getInstallErrorMessage( + makeErr("host has fleetd installed from another source") + ); + expect(result).toContain("fleetd installed"); + }); + + it("returns macOS-only install message", () => { + const result = getInstallErrorMessage( + makeErr("software can be installed only on darwin") + ); + expect(result).toBe( + "Couldn't install. Software can be installed only on macOS." + ); + }); + + it("returns MDM turned off message as-is", () => { + const result = getInstallErrorMessage(makeErr("MDM is turned off.")); + expect(result).toBe("MDM is turned off"); + }); + + it("returns no available licenses message as-is", () => { + const result = getInstallErrorMessage(makeErr("No available licenses.")); + expect(result).toBe("No available licenses"); + }); + + it("returns unresolvable Fleet variable message", () => { + const result = getInstallErrorMessage( + makeErr( + "apple_mdm: unresolvable Fleet variable in managed app configuration" + ) + ); + expect(result).toBe( + "Couldn't install. Couldn't resolve a Fleet variable in the managed app configuration for this host." + ); + }); + + it("returns default message for unknown errors", () => { + const result = getInstallErrorMessage(makeErr("something unexpected")); + expect(result).toBe("Couldn't install. Please try again."); + }); + + it("returns default message when no reason can be extracted", () => { + expect(getInstallErrorMessage({})).toBe( + "Couldn't install. Please try again." + ); + }); +}); diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tsx index 27ed72d6c5..07cc095c05 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/helpers.tsx @@ -62,6 +62,10 @@ export const getInstallErrorMessage = (e: unknown) => { return createOnlyInstallableOnMacOSMessage(reason); } else if (reason.includes("VPP token expired")) { return createVPPTokenExpiredMessage(); + } else if ( + reason.includes("unresolvable Fleet variable in managed app configuration") + ) { + return `${INSTALL_SOFTWARE_ERROR_PREFIX} Couldn't resolve a Fleet variable in the managed app configuration for this host.`; } else if (showAPIMessage(reason)) { return reason; }