1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-12-06 09:01:04 +01:00

firefox: add extension permissions (#7402)

Adds extension permissions as suggested in
https://github.com/nix-community/home-manager/issues/7001.
Adds the 'profiles.<name>.extensions.settings.<name>.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.<name>.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.<name>.settings'
was set instead of checking whether 'extensions.settings' was set.

---------

Co-authored-by: Robert Helgesson <robert@rycee.net>
Co-authored-by: awwpotato <awwpotato@voidq.com>
This commit is contained in:
bricked 2025-07-10 20:33:18 +00:00 committed by GitHub
parent fb12dbbce3
commit d52da303ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 214 additions and 102 deletions

View file

@ -80,6 +80,9 @@ let
if lib.isBool pref || lib.isInt pref || lib.isString pref then pref else builtins.toJSON pref 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 = mkUserJs =
prePrefs: prefs: extraPrefs: bookmarksFile: extensions: prePrefs: prefs: extraPrefs: bookmarksFile: extensions:
let let
@ -88,7 +91,7 @@ let
"browser.bookmarks.file" = toString bookmarksFile; "browser.bookmarks.file" = toString bookmarksFile;
"browser.places.importBookmarksHTML" = true; "browser.places.importBookmarksHTML" = true;
} }
// lib.optionalAttrs (extensions != { }) { // lib.optionalAttrs (extensionSettingsNeedForce extensions) {
"extensions.webextensions.ExtensionStorageIDB.enabled" = false; "extensions.webextensions.ExtensionStorageIDB.enabled" = false;
} }
// prefs; // prefs;
@ -356,6 +359,12 @@ in
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
{ config, name, ... }: { config, name, ... }:
let
profilePath = modulePath ++ [
"profiles"
name
];
in
{ {
imports = [ (pkgs.path + "/nixos/modules/misc/assertions.nix") ]; imports = [ (pkgs.path + "/nixos/modules/misc/assertions.nix") ];
@ -721,7 +730,19 @@ in
options = { options = {
settings = mkOption { settings = mkOption {
type = types.attrsOf jsonFormat.type; 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
<https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>
for a list of relevant permissions.
'';
}; };
force = mkOption { force = mkOption {
type = types.bool; type = types.bool;
@ -763,36 +784,54 @@ in
}; };
config = { config = {
assertions = [ assertions =
(mkNoDuplicateAssertion config.containers "container") [
{ (mkNoDuplicateAssertion config.containers "container")
assertion = config.extensions.settings == { } || config.extensions.force; {
message = '' assertion = !(extensionSettingsNeedForce config.extensions.settings) || config.extensions.force;
Using '${ message = ''
lib.showAttrPath ( Using '${lib.showOption profilePath}.extensions.settings' will override all
modulePath previous extensions settings. Enable
++ [ '${lib.showOption profilePath}.extensions.force' to acknowledge this.
"profiles" '';
config.name }
"extensions" ]
"settings" ++ (builtins.concatMap (
] { name, value }:
) let
}' will override all previous extensions settings. packages = builtins.filter (pkg: pkg.addonId == name) config.extensions.packages;
Enable '${ package = builtins.head packages;
lib.showAttrPath ( unauthorized = lib.subtractLists value.permissions package.meta.mozPermissions;
modulePath in
++ [ [
"profiles" {
config.name assertion = value.permissions == null || length packages == 1;
"extensions" message = ''
"force" Must have exactly one extension with addonId '${name}'
] in '${lib.showOption profilePath}.extensions.packages' but found ${toString (length packages)}.
) '';
}' to acknowledge this. }
'';
} {
] ++ config.bookmarks.assertions; 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 ( ++ lib.flip mapAttrsToList cfg.profiles (
_: profile: _: profile:
# Merge the regular profile settings with extension settings # Merge the regular profile settings with extension settings
mkMerge ( mkMerge [
[ {
{ "${cfg.profilesPath}/${profile.path}/.keep".text = "";
"${cfg.profilesPath}/${profile.path}/.keep".text = "";
"${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") ( "${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") (
let let
key = if builtins.isString profile.userChrome then "text" else "source"; key = if builtins.isString profile.userChrome then "text" else "source";
in 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 let
key = if builtins.isString profile.userContent then "text" else "source"; extensionsEnvPkg = pkgs.buildEnv {
name = "hm-firefox-extensions";
paths = profile.extensions.packages;
};
in in
{ "${extensionsEnvPkg}/share/mozilla/${extensionPath}";
"${key}" = profile.userContent; recursive = true;
} force = true;
); };
}
"${cfg.profilesPath}/${profile.path}/user.js" = (mkMerge (
mkIf mapAttrsToList (
( name: settingConfig:
profile.preConfig != "" mkIf (settingConfig.settings != { }) {
|| profile.settings != { } "${cfg.profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = {
|| profile.extraConfig != "" force = settingConfig.force || profile.extensions.force;
|| profile.bookmarks.configFile != null text = lib.generators.toJSON { } settingConfig.settings;
|| profile.extensions.settings != { } };
) }
{ ) 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
)
)
)
) )
); );
} }

View file

@ -19,6 +19,7 @@ builtins.mapAttrs
"${name}-profiles-containers-id-out-of-range" = ./profiles/containers/id-out-of-range.nix; "${name}-profiles-containers-id-out-of-range" = ./profiles/containers/id-out-of-range.nix;
"${name}-profiles-duplicate-ids" = ./profiles/duplicate-ids.nix; "${name}-profiles-duplicate-ids" = ./profiles/duplicate-ids.nix;
"${name}-profiles-extensions" = ./profiles/extensions; "${name}-profiles-extensions" = ./profiles/extensions;
"${name}-profiles-extensions-assertions" = ./profiles/extensions/assertions.nix;
"${name}-profiles-overwrite" = ./profiles/overwrite; "${name}-profiles-overwrite" = ./profiles/overwrite;
"${name}-profiles-search" = ./profiles/search; "${name}-profiles-search" = ./profiles/search;
"${name}-profiles-settings" = ./profiles/settings; "${name}-profiles-settings" = ./profiles/settings;

View file

@ -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"
"<all_urls>"
"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","<all_urls>","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.
''
];
}
);
}