diff --git a/.changeset/mighty-forks-pump.md b/.changeset/mighty-forks-pump.md new file mode 100644 index 00000000000..8feb9f0494b --- /dev/null +++ b/.changeset/mighty-forks-pump.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": minor +--- + +feat: add support for AppArmor with template profile and configuration property diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index 9a438419de4..8021f5a9fea 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -519,12 +519,21 @@ "additionalProperties": false, "properties": { "afterInstall": { + "description": "File path to script to be passed to FPM for `--after-install` arg.", "type": [ "null", "string" ] }, "afterRemove": { + "description": "File path to script to be passed to FPM for `--after-remove` arg.", + "type": [ + "null", + "string" + ] + }, + "appArmorProfile": { + "description": "File path to custom AppArmor profile (Ubuntu 24+)", "type": [ "null", "string" @@ -2050,12 +2059,21 @@ "additionalProperties": false, "properties": { "afterInstall": { + "description": "File path to script to be passed to FPM for `--after-install` arg.", "type": [ "null", "string" ] }, "afterRemove": { + "description": "File path to script to be passed to FPM for `--after-remove` arg.", + "type": [ + "null", + "string" + ] + }, + "appArmorProfile": { + "description": "File path to custom AppArmor profile (Ubuntu 24+)", "type": [ "null", "string" diff --git a/packages/app-builder-lib/src/options/linuxOptions.ts b/packages/app-builder-lib/src/options/linuxOptions.ts index 77e4882cb36..8157174c20f 100644 --- a/packages/app-builder-lib/src/options/linuxOptions.ts +++ b/packages/app-builder-lib/src/options/linuxOptions.ts @@ -122,8 +122,18 @@ export interface LinuxTargetSpecificOptions extends CommonLinuxOptions, TargetSp readonly vendor?: string | null readonly maintainer?: string | null + /** + * File path to script to be passed to FPM for `--after-install` arg. + */ readonly afterInstall?: string | null + /** + * File path to script to be passed to FPM for `--after-remove` arg. + */ readonly afterRemove?: string | null + /** + * File path to custom AppArmor profile (Ubuntu 24+) + */ + readonly appArmorProfile?: string | null /** * *Advanced only* The [fpm](https://fpm.readthedocs.io/en/latest/cli-reference.html) options. diff --git a/packages/app-builder-lib/src/targets/FpmTarget.ts b/packages/app-builder-lib/src/targets/FpmTarget.ts index 46baf32e8c6..2ad603bc5a8 100644 --- a/packages/app-builder-lib/src/targets/FpmTarget.ts +++ b/packages/app-builder-lib/src/targets/FpmTarget.ts @@ -1,6 +1,6 @@ import { Arch, executeAppBuilder, getArchSuffix, log, TmpDir, toLinuxArchString, use, serializeToYaml, asArray } from "builder-util" import { unlinkIfExists } from "builder-util" -import { outputFile, stat } from "fs-extra" +import { copyFile, outputFile, stat } from "fs-extra" import { mkdir, readFile } from "fs/promises" import * as path from "path" import { smarten } from "../appInfo" @@ -26,10 +26,16 @@ interface FpmOptions { url: string } +interface ScriptFiles { + afterRemove: string + afterInstall: string + appArmor: string +} + export default class FpmTarget extends Target { readonly options: LinuxTargetSpecificOptions = { ...this.packager.platformSpecificBuildOptions, ...(this.packager.config as any)[this.name] } - private readonly scriptFiles: Promise> + private readonly scriptFiles: Promise constructor( name: string, @@ -42,7 +48,7 @@ export default class FpmTarget extends Target { this.scriptFiles = this.createScripts() } - private async createScripts(): Promise> { + private async createScripts(): Promise { const defaultTemplatesDir = getTemplatePath("linux") const packager = this.packager @@ -61,10 +67,11 @@ export default class FpmTarget extends Target { return path.resolve(packager.projectDir, value) } - return await Promise.all([ - writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterInstall, "after-install.tpl"), templateOptions), - writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterRemove, "after-remove.tpl"), templateOptions), - ]) + return { + afterInstall: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterInstall, "after-install.tpl"), templateOptions), + afterRemove: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterRemove, "after-remove.tpl"), templateOptions), + appArmor: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.appArmorProfile, "apparmor-profile.tpl"), templateOptions), + } } checkOptions(): Promise { @@ -130,13 +137,13 @@ export default class FpmTarget extends Target { if (packager.packagerOptions.prepackaged != null) { await mkdir(this.outDir, { recursive: true }) } + const linuxDistType = packager.packagerOptions.prepackaged || path.join(this.outDir, `linux${getArchSuffix(arch)}-unpacked`) + const resourceDir = packager.getResourcesDir(linuxDistType) const publishConfig = this.supportsAutoUpdate(target) ? await getAppUpdatePublishConfiguration(packager, arch, false /* in any case validation will be done on publish */) : null if (publishConfig != null) { - const linuxDistType = this.packager.packagerOptions.prepackaged || path.join(this.outDir, `linux${getArchSuffix(arch)}-unpacked`) - const resourceDir = packager.getResourcesDir(linuxDistType) log.info({ resourceDir: log.filePath(resourceDir) }, `adding autoupdate files for: ${target}. (Beta feature)`) await outputFile(path.join(resourceDir, "app-update.yml"), serializeToYaml(publishConfig)) // Extra file needed for auto-updater to detect installation method @@ -144,6 +151,11 @@ export default class FpmTarget extends Target { } const scripts = await this.scriptFiles + + // Install AppArmor support for ubuntu 24+ + // https://github.com/electron-userland/electron-builder/issues/8635 + await copyFile(scripts.appArmor, path.join(resourceDir, "apparmor-profile")) + const appInfo = packager.appInfo const options = this.options const synopsis = options.synopsis @@ -151,9 +163,9 @@ export default class FpmTarget extends Target { "--architecture", toLinuxArchString(arch, target), "--after-install", - scripts[0], + scripts.afterInstall, "--after-remove", - scripts[1], + scripts.afterRemove, "--description", smarten(target === "rpm" ? this.helper.getDescription(options) : `${synopsis || ""}\n ${this.helper.getDescription(options)}`), "--version", diff --git a/packages/app-builder-lib/templates/linux/after-install.tpl b/packages/app-builder-lib/templates/linux/after-install.tpl index 0c9b9459062..7433249abf7 100644 --- a/packages/app-builder-lib/templates/linux/after-install.tpl +++ b/packages/app-builder-lib/templates/linux/after-install.tpl @@ -25,3 +25,30 @@ fi if hash update-desktop-database 2>/dev/null; then update-desktop-database /usr/share/applications || true fi + +# Install apparmor profile. (Ubuntu 24+) +# First check if the version of AppArmor running on the device supports our profile. +# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0. +# In that case, we just skip installing the profile since the app runs fine without it on 22.04. +# +# Those apparmor_parser flags are akin to performing a dry run of loading a profile. +# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles +# +# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility. +# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile +APPARMOR_PROFILE_SOURCE='/opt/${sanitizedProductName}/resources/apparmor-profile' +APPARMOR_PROFILE_TARGET='/etc/apparmor.d/${executable}' +if test -d "/etc/apparmor.d"; then + if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then + cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET" + + if hash apparmor_parser 2>/dev/null; then + # Extra flags taken from dh_apparmor: + # > By using '-W -T' we ensure that any abstraction updates are also pulled in. + # https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport + apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET" + fi + else + echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile" + fi +fi \ No newline at end of file diff --git a/packages/app-builder-lib/templates/linux/after-remove.tpl b/packages/app-builder-lib/templates/linux/after-remove.tpl index a3004ebbd0e..19b3decabe1 100644 --- a/packages/app-builder-lib/templates/linux/after-remove.tpl +++ b/packages/app-builder-lib/templates/linux/after-remove.tpl @@ -6,3 +6,10 @@ if type update-alternatives >/dev/null 2>&1; then else rm -f '/usr/bin/${executable}' fi + +APPARMOR_PROFILE_DEST='/etc/apparmor.d/${executable}' + +# Remove apparmor profile. +if [ -f "$APPARMOR_PROFILE_DEST" ]; then + rm -f "$APPARMOR_PROFILE_DEST" +fi \ No newline at end of file diff --git a/packages/app-builder-lib/templates/linux/apparmor-profile.tpl b/packages/app-builder-lib/templates/linux/apparmor-profile.tpl new file mode 100644 index 00000000000..3769b7963b8 --- /dev/null +++ b/packages/app-builder-lib/templates/linux/apparmor-profile.tpl @@ -0,0 +1,9 @@ +abi , +include + +profile ${executable} "/opt/${sanitizedProductName}/${executable}" flags=(unconfined) { + userns, + + # Site-specific additions and overrides. See local/README for details. + include if exists +} \ No newline at end of file diff --git a/test/snapshots/PublishManagerTest.js.snap b/test/snapshots/PublishManagerTest.js.snap index 0c0ec7ad561..c453041e114 100644 --- a/test/snapshots/PublishManagerTest.js.snap +++ b/test/snapshots/PublishManagerTest.js.snap @@ -58,6 +58,7 @@ exports[`custom provider 2`] = ` "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app-update.yml", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/opt/Test App ßW/resources/package-type", "/usr/share/applications/", "/usr/share/applications/testapp.desktop", diff --git a/test/snapshots/linux/debTest.js.snap b/test/snapshots/linux/debTest.js.snap index 600e070cbae..71bb7417e37 100644 --- a/test/snapshots/linux/debTest.js.snap +++ b/test/snapshots/linux/debTest.js.snap @@ -41,6 +41,7 @@ exports[`arm 2`] = ` "/usr/share/", "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/testapp.desktop", "/usr/share/doc/", @@ -115,6 +116,7 @@ exports[`arm 5`] = ` "/usr/share/", "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/testapp.desktop", "/usr/share/doc/", @@ -200,6 +202,7 @@ exports[`custom depends 2`] = ` "/usr/share/", "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/Boo.desktop", "/usr/share/doc/", @@ -285,6 +288,7 @@ exports[`deb 2`] = ` "/usr/share/", "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/testapp.desktop", "/usr/share/doc/", @@ -370,6 +374,7 @@ exports[`deb file associations 2`] = ` "/usr/share/", "/opt/Test App ßW/resources/", "/opt/Test App ßW/resources/app.asar", + "/opt/Test App ßW/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/testapp.desktop", "/usr/share/doc/", @@ -469,6 +474,7 @@ exports[`executable path in postinst script 2`] = ` "/usr/share/", "/opt/foo/resources/", "/opt/foo/resources/app.asar", + "/opt/foo/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/Boo.desktop", "/usr/share/doc/", @@ -544,6 +550,33 @@ fi if hash update-desktop-database 2>/dev/null; then update-desktop-database /usr/share/applications || true +fi + +# Install apparmor profile. (Ubuntu 24+) +# First check if the version of AppArmor running on the device supports our profile. +# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0. +# In that case, we just skip installing the profile since the app runs fine without it on 22.04. +# +# Those apparmor_parser flags are akin to performing a dry run of loading a profile. +# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles +# +# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility. +# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile +APPARMOR_PROFILE_SOURCE='/opt/foo/resources/apparmor-profile' +APPARMOR_PROFILE_TARGET='/etc/apparmor.d/Boo' +if test -d "/etc/apparmor.d"; then + if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then + cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET" + + if hash apparmor_parser 2>/dev/null; then + # Extra flags taken from dh_apparmor: + # > By using '-W -T' we ensure that any abstraction updates are also pulled in. + # https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport + apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET" + fi + else + echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile" + fi fi" `; @@ -597,6 +630,7 @@ exports[`no quotes for safe exec name 3`] = ` "/usr/share/", "/opt/foo/resources/", "/opt/foo/resources/app.asar", + "/opt/foo/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/Boo.desktop", "/usr/share/doc/", @@ -682,6 +716,7 @@ exports[`top-level exec name 2`] = ` "/usr/share/", "/opt/foo/resources/", "/opt/foo/resources/app.asar", + "/opt/foo/resources/apparmor-profile", "/usr/share/applications/", "/usr/share/applications/Boo.desktop", "/usr/share/doc/",