From d52da303efeb39d67fdedd08f7d8fc8efd5f4332 Mon Sep 17 00:00:00 2001 From: bricked Date: Thu, 10 Jul 2025 20:33:18 +0000 Subject: [PATCH] firefox: add extension permissions (#7402) Adds extension permissions as suggested in https://github.com/nix-community/home-manager/issues/7001. Adds the 'profiles..extensions.settings..permissions' to Firefox derivatives. If set, this option adds an assertion that fails if an extension package requests permissions that weren't added to the permissions option. In order to not require 'profiles..extensions.force' to be set when only permissions, but no extension settings were defined, the relevant assertions were changed. They now check whether any 'extensions.settings..settings' was set instead of checking whether 'extensions.settings' was set. --------- Co-authored-by: Robert Helgesson Co-authored-by: awwpotato --- modules/programs/firefox/mkFirefoxModule.nix | 241 ++++++++++-------- tests/modules/programs/firefox/common.nix | 1 + .../profiles/extensions/assertions.nix | 74 ++++++ 3 files changed, 214 insertions(+), 102 deletions(-) create mode 100644 tests/modules/programs/firefox/profiles/extensions/assertions.nix diff --git a/modules/programs/firefox/mkFirefoxModule.nix b/modules/programs/firefox/mkFirefoxModule.nix index 2ec867a46..b86de9f51 100644 --- a/modules/programs/firefox/mkFirefoxModule.nix +++ b/modules/programs/firefox/mkFirefoxModule.nix @@ -80,6 +80,9 @@ let if lib.isBool pref || lib.isInt pref || lib.isString pref then pref else builtins.toJSON pref ); + extensionSettingsNeedForce = + extensionSettings: builtins.any (ext: ext.settings != { }) (attrValues extensionSettings); + mkUserJs = prePrefs: prefs: extraPrefs: bookmarksFile: extensions: let @@ -88,7 +91,7 @@ let "browser.bookmarks.file" = toString bookmarksFile; "browser.places.importBookmarksHTML" = true; } - // lib.optionalAttrs (extensions != { }) { + // lib.optionalAttrs (extensionSettingsNeedForce extensions) { "extensions.webextensions.ExtensionStorageIDB.enabled" = false; } // prefs; @@ -356,6 +359,12 @@ in type = types.attrsOf ( types.submodule ( { config, name, ... }: + let + profilePath = modulePath ++ [ + "profiles" + name + ]; + in { imports = [ (pkgs.path + "/nixos/modules/misc/assertions.nix") ]; @@ -721,7 +730,19 @@ in options = { settings = mkOption { type = types.attrsOf jsonFormat.type; - description = "Json formatted options for the specified extensionID"; + default = { }; + description = "Json formatted options for this extension."; + }; + permissions = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ "activeTab" ]; + defaultText = "Any permissions"; + description = '' + Allowed permissions for this extension. See + + for a list of relevant permissions. + ''; }; force = mkOption { type = types.bool; @@ -763,36 +784,54 @@ in }; config = { - assertions = [ - (mkNoDuplicateAssertion config.containers "container") - { - assertion = config.extensions.settings == { } || config.extensions.force; - message = '' - Using '${ - lib.showAttrPath ( - modulePath - ++ [ - "profiles" - config.name - "extensions" - "settings" - ] - ) - }' will override all previous extensions settings. - Enable '${ - lib.showAttrPath ( - modulePath - ++ [ - "profiles" - config.name - "extensions" - "force" - ] - ) - }' to acknowledge this. - ''; - } - ] ++ config.bookmarks.assertions; + assertions = + [ + (mkNoDuplicateAssertion config.containers "container") + { + assertion = !(extensionSettingsNeedForce config.extensions.settings) || config.extensions.force; + message = '' + Using '${lib.showOption profilePath}.extensions.settings' will override all + previous extensions settings. Enable + '${lib.showOption profilePath}.extensions.force' to acknowledge this. + ''; + } + ] + ++ (builtins.concatMap ( + { name, value }: + let + packages = builtins.filter (pkg: pkg.addonId == name) config.extensions.packages; + package = builtins.head packages; + unauthorized = lib.subtractLists value.permissions package.meta.mozPermissions; + in + [ + { + assertion = value.permissions == null || length packages == 1; + message = '' + Must have exactly one extension with addonId '${name}' + in '${lib.showOption profilePath}.extensions.packages' but found ${toString (length packages)}. + ''; + } + + { + assertion = value.permissions == null || length packages != 1 || unauthorized == [ ]; + message = '' + Extension ${name} requests permissions that weren't + authorized: ${builtins.toJSON unauthorized}. + Consider adding the missing permissions to + '${ + lib.showAttrPath ( + profilePath + ++ [ + "extensions" + name + ] + ) + }.permissions'. + ''; + } + ] + ) (lib.attrsToList config.extensions.settings)) + ++ config.bookmarks.assertions; }; } ) @@ -888,82 +927,80 @@ in ++ lib.flip mapAttrsToList cfg.profiles ( _: profile: # Merge the regular profile settings with extension settings - mkMerge ( - [ - { - "${cfg.profilesPath}/${profile.path}/.keep".text = ""; + mkMerge [ + { + "${cfg.profilesPath}/${profile.path}/.keep".text = ""; - "${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") ( - let - key = if builtins.isString profile.userChrome then "text" else "source"; - in + "${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") ( + let + key = if builtins.isString profile.userChrome then "text" else "source"; + in + { + "${key}" = profile.userChrome; + } + ); + + "${cfg.profilesPath}/${profile.path}/chrome/userContent.css" = mkIf (profile.userContent != "") ( + let + key = if builtins.isString profile.userContent then "text" else "source"; + in + { + "${key}" = profile.userContent; + } + ); + + "${cfg.profilesPath}/${profile.path}/user.js" = + mkIf + ( + profile.preConfig != "" + || profile.settings != { } + || profile.extraConfig != "" + || profile.bookmarks.configFile != null + || extensionSettingsNeedForce profile.extensions.settings + ) { - "${key}" = profile.userChrome; - } - ); + text = + mkUserJs profile.preConfig profile.settings profile.extraConfig profile.bookmarks.configFile + profile.extensions.settings; + }; - "${cfg.profilesPath}/${profile.path}/chrome/userContent.css" = mkIf (profile.userContent != "") ( + "${cfg.profilesPath}/${profile.path}/containers.json" = mkIf (profile.containers != { }) { + text = mkContainersJson profile.containers; + force = profile.containersForce; + }; + + "${cfg.profilesPath}/${profile.path}/search.json.mozlz4" = mkIf (profile.search.enable) { + enable = profile.search.enable; + force = profile.search.force; + source = profile.search.file; + }; + + "${cfg.profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions.packages != [ ]) { + source = let - key = if builtins.isString profile.userContent then "text" else "source"; + extensionsEnvPkg = pkgs.buildEnv { + name = "hm-firefox-extensions"; + paths = profile.extensions.packages; + }; in - { - "${key}" = profile.userContent; - } - ); + "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; + recursive = true; + force = true; + }; + } - "${cfg.profilesPath}/${profile.path}/user.js" = - mkIf - ( - profile.preConfig != "" - || profile.settings != { } - || profile.extraConfig != "" - || profile.bookmarks.configFile != null - || profile.extensions.settings != { } - ) - { - text = - mkUserJs profile.preConfig profile.settings profile.extraConfig profile.bookmarks.configFile - profile.extensions.settings; - }; - - "${cfg.profilesPath}/${profile.path}/containers.json" = mkIf (profile.containers != { }) { - text = mkContainersJson profile.containers; - force = profile.containersForce; - }; - - "${cfg.profilesPath}/${profile.path}/search.json.mozlz4" = mkIf (profile.search.enable) { - enable = profile.search.enable; - force = profile.search.force; - source = profile.search.file; - }; - - "${cfg.profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions.packages != [ ]) { - source = - let - extensionsEnvPkg = pkgs.buildEnv { - name = "hm-firefox-extensions"; - paths = profile.extensions.packages; - }; - in - "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; - recursive = true; - force = true; - }; - } - ] - ++ - # Add extension settings as separate attributes - optional (profile.extensions.settings != { }) ( - mkMerge ( - mapAttrsToList (name: settingConfig: { - "${cfg.profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = { - force = settingConfig.force || profile.extensions.force; - text = lib.generators.toJSON { } settingConfig.settings; - }; - }) profile.extensions.settings - ) - ) - ) + (mkMerge ( + mapAttrsToList ( + name: settingConfig: + mkIf (settingConfig.settings != { }) { + "${cfg.profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = { + force = settingConfig.force || profile.extensions.force; + text = lib.generators.toJSON { } settingConfig.settings; + }; + } + ) profile.extensions.settings + )) + ] ) ); } diff --git a/tests/modules/programs/firefox/common.nix b/tests/modules/programs/firefox/common.nix index 35282c041..f7f4a8cd0 100644 --- a/tests/modules/programs/firefox/common.nix +++ b/tests/modules/programs/firefox/common.nix @@ -19,6 +19,7 @@ builtins.mapAttrs "${name}-profiles-containers-id-out-of-range" = ./profiles/containers/id-out-of-range.nix; "${name}-profiles-duplicate-ids" = ./profiles/duplicate-ids.nix; "${name}-profiles-extensions" = ./profiles/extensions; + "${name}-profiles-extensions-assertions" = ./profiles/extensions/assertions.nix; "${name}-profiles-overwrite" = ./profiles/overwrite; "${name}-profiles-search" = ./profiles/search; "${name}-profiles-settings" = ./profiles/settings; diff --git a/tests/modules/programs/firefox/profiles/extensions/assertions.nix b/tests/modules/programs/firefox/profiles/extensions/assertions.nix new file mode 100644 index 000000000..fe5aab78c --- /dev/null +++ b/tests/modules/programs/firefox/profiles/extensions/assertions.nix @@ -0,0 +1,74 @@ +modulePath: +{ config, lib, ... }: + +let + + firefoxMockOverlay = import ../../setup-firefox-mock-overlay.nix modulePath; + + uBlockStubPkg = config.lib.test.mkStubPackage { + name = "ublock-origin-dummy"; + extraAttrs = { + addonId = "uBlock0@raymondhill.net"; + meta.mozPermissions = [ + "privacy" + "storage" + "tabs" + "" + "http://*/*" + "https://github.com/*" + ]; + }; + }; +in +{ + imports = [ firefoxMockOverlay ]; + + config = lib.mkIf config.test.enableBig ( + lib.setAttrByPath modulePath { + enable = true; + profiles.extensions = { + extensions = { + packages = [ uBlockStubPkg ]; + settings = { + "uBlock0@raymondhill.net" = { + settings = { + selectedFilterLists = [ + "ublock-filters" + "ublock-badware" + "ublock-privacy" + "ublock-unbreak" + "ublock-quick-fixes" + ]; + }; + permissions = [ + "alarms" + "tabs" + "https://github.com/*" + ]; + }; + "unknown@example.com".permissions = [ ]; + }; + }; + }; + } + // { + test.asserts.assertions.expected = [ + '' + Using '${lib.showOption modulePath}.profiles.extensions.extensions.settings' will override all + previous extensions settings. Enable + '${lib.showOption modulePath}.profiles.extensions.extensions.force' to acknowledge this. + '' + '' + Extension uBlock0@raymondhill.net requests permissions that weren't + authorized: ["privacy","storage","","http://*/*"]. + Consider adding the missing permissions to + '${lib.showOption modulePath}.profiles.extensions.extensions."uBlock0@raymondhill.net".permissions'. + '' + '' + Must have exactly one extension with addonId 'unknown@example.com' + in '${lib.showOption modulePath}.profiles.extensions.extensions.packages' but found 0. + '' + ]; + } + ); +}