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. + '' + ]; + } + ); +}